Browse Source

Merge branch 'master' into feat/input-new-style

pull/15265/head
Maksym Tsymbarov 7 months ago
committed by GitHub
parent
commit
c944858859
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .gitignore
  2. 2
      application/src/main/data/json/system/widget_types/label_widget.json
  3. 5
      application/src/main/data/upgrade/basic/schema_update.sql
  4. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  5. 64
      application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersCustomizer.java
  6. 56
      application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersProperties.java
  7. 267
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  8. 12
      application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java
  9. 19
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  10. 3
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  11. 2
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  12. 22
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  13. 6
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  14. 2
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  15. 28
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  16. 19
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  17. 8
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  18. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  19. 40
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  20. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  21. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  22. 105
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java
  23. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java
  24. 63
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java
  25. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  26. 11
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  28. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java
  29. 23
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  30. 4
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  31. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
  32. 8
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  33. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
  34. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java
  35. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
  36. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java
  37. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
  38. 4
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
  39. 75
      application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java
  40. 94
      application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java
  41. 3
      application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java
  42. 149
      application/src/main/resources/thingsboard.yml
  43. 11
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  44. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  45. 71
      application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java
  46. 33
      application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
  47. 4
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  48. 12
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  49. 24
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  50. 21
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  51. 266
      application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java
  52. 106
      application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
  53. 238
      application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java
  54. 3
      application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
  55. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  56. 272
      application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java
  57. 8
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  58. 10
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  59. 3
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  60. 8
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  61. 144
      application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java
  62. 136
      application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java
  63. 9
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  64. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
  65. 8
      common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java
  66. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
  67. 16
      common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java
  68. 11
      common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java
  69. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java
  70. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  71. 31
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  72. 3
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  73. 8
      common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
  74. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java
  75. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java
  76. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java
  77. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
  78. 5
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java
  79. 5
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java
  80. 5
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java
  81. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
  82. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
  83. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java
  84. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java
  85. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java
  86. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
  87. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java
  88. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java
  89. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
  90. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
  91. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
  92. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java
  93. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java
  94. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java
  95. 43
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
  96. 1
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  97. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
  98. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java
  99. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
  100. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java

3
.gitignore

@ -37,4 +37,5 @@ rebuild-docker.sh
*/.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

5
application/src/main/data/upgrade/basic/schema_update.sql

@ -14,3 +14,8 @@
-- limitations under the License.
--
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar;
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END

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:}")
private List<String> ssrfAdditionalBlockedHosts;
@Value("${actors.rule.external.ssrf_allowed_hosts:}")
private List<String> ssrfAllowedHosts;
@PostConstruct
public void init() {
this.localCacheType = "caffeine".equals(cacheType);
SsrfProtectionValidator.setEnabled(ssrfProtectionEnabled);
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}")

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;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
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.OperationCustomizer;
import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer;
import org.springdoc.core.utils.SpringDocUtils;
import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties;
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.Modifier;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -90,6 +97,8 @@ public class SwaggerConfiguration {
@PostConstruct
public void configureModelResolver() {
ModelResolver.enumsAsRef = true;
SpringDocUtils.getConfig().replaceWithSchema(ByteBuffer.class,
new Schema<String>().type("string").format("byte"));
}
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 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/**}")
private String apiPath;
@Value("${swagger.security_path_regex}")
@ -304,23 +318,21 @@ public class SwaggerConfiguration {
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 {
var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType);
var orderedNames = resolvePropertyOrder(cls, beanDesc);
if (!orderedNames.isEmpty()) {
@SuppressWarnings("unchecked")
Map<String, Schema> current = schema.getProperties();
var reordered = new LinkedHashMap<String, Schema>();
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);
String schemaName = resolveSchemaName(javaType);
schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc));
Set<String> ownProps = computeOwnPropNames(cls, beanDesc);
if (!ownProps.isEmpty()) {
schemaOwnProps.put(schemaName, ownProps);
}
} catch (Exception ignored) {
log.trace("Failed to resolve property order for {}", cls.getName(), ignored);
} catch (Exception e) {
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
paths.values().stream()
.flatMap(pathItem -> pathItem.readOperationsMap().values().stream())
@ -530,6 +557,9 @@ public class SwaggerConfiguration {
if (prop.getDescription() != null) {
refSchema.setDescription(prop.getDescription());
}
if (prop.getReadOnly() != null) {
refSchema.setReadOnly(prop.getReadOnly());
}
schema.getProperties().put(propName, refSchema);
log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName);
}
@ -540,7 +570,7 @@ public class SwaggerConfiguration {
private String tagItemFromPathItem(PathItem item) {
var operations = item.readOperationsMap().values();
var operation = operations.stream().findAny();
var operation = operations.stream().findFirst();
if (operation.isPresent()) {
var tags = operation.get().getTags();
if (tags != null && !tags.isEmpty()) {
@ -676,15 +706,206 @@ public class SwaggerConfiguration {
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) {
// 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<String, String> fieldToJsonName = new LinkedHashMap<>();
LinkedHashSet<String> getterOnlyNames = new LinkedHashSet<>();
for (var prop : beanDesc.findProperties()) {
if (prop.getField() != null) {
if (prop.getField() != null && prop.couldSerialize()) {
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
ordered.addAll(getterOnlyNames);
// Return only field-backed properties in declaration order.
// 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;
}

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

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

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

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

3
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -60,7 +60,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
}
registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*");
registry.addHandler(wsHandler, WS_API_MAPPING)
.setAllowedOriginPatterns("*");
}
}

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

@ -68,7 +68,7 @@ public class ApiKeyController extends BaseController {
private final ApiKeyService apiKeyService;
@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')")
@PostMapping(value = "/apiKey")
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 +
"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" +
"- 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" +
"- 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" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> 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" +
"- Credentials type: **\"X509\"** with **device profile ID** below: \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" +
"- 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" +
"- 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" +
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. " +
@ -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" +
"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" +
"- 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" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> 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" +
"- Credentials type: **\"X509\"** with **device profile ID** below: \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" +
"- 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" +
"- 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" +
DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\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" +
" \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" +
" \"name\": \"Devices and assets - v2.0\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" +
" \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" },\n" +
" {\n" +
" \"timestamp\": 1655198528000,\n" +
" \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" +
" \"name\": \"Update my device\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" +
" \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" },\n" +
" {\n" +
" \"timestamp\": 1655198280000,\n" +
" \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" +
" \"name\": \"Devices and assets - v1.0\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" +
" \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" }\n" +
" ],\n" +
" \"totalPages\": 1,\n" +

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

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

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

@ -52,6 +52,7 @@ import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -100,6 +101,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private RateLimitService rateLimitService;
@Autowired
private JwtAuthenticationProvider authenticationProvider;
@Autowired
private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;
@Value("${server.ws.send_timeout:5000}")
private long sendTimeout;
@ -194,7 +197,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
log.trace("{} Authenticating session", sessionRef);
SecurityUser securityCtx;
try {
securityCtx = authenticationProvider.authenticate(authCmd.getToken());
if (StringUtils.isNotEmpty(authCmd.getApiKey())) {
securityCtx = apiKeyAuthenticationProvider.authenticate(authCmd.getApiKey());
} else {
securityCtx = authenticationProvider.authenticate(authCmd.getToken());
}
} catch (Exception e) {
close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage()));
return;
@ -328,9 +335,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
SecurityUser securityCtx = null;
String token = StringUtils.substringAfter(session.getUri().getQuery(), "token=");
if (StringUtils.isNotEmpty(token)) {
securityCtx = authenticationProvider.authenticate(token);
String query = session.getUri().getQuery();
if (query != null) {
String token = extractQueryParam(query, "token");
if (StringUtils.isNotEmpty(token)) {
securityCtx = authenticationProvider.authenticate(token);
}
}
return WebSocketSessionRef.builder()
.sessionId(UUID.randomUUID().toString())
@ -341,6 +351,15 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
.build();
}
private String extractQueryParam(String query, String paramName) {
for (String param : query.split("&")) {
if (param.startsWith(paramName + "=")) {
return param.substring(paramName.length() + 1);
}
}
return null;
}
private SessionMetaData getSessionMd(String internalSessionId) {
SessionMetaData sessionMd = internalSessionMap.get(internalSessionId);
if (sessionMd == null) {
@ -482,6 +501,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
}
}
@Override

19
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.exception;
import jakarta.persistence.PersistenceException;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
@ -162,8 +164,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleAuthenticationException(authenticationException, response);
} else if (exception instanceof MaxPayloadSizeExceededException maxPayloadSizeExceededException) {
handleMaxPayloadSizeExceededException(response, maxPayloadSizeExceededException);
} else if (exception instanceof DataAccessException e) {
handleDatabaseException(e, response);
} else if (exception instanceof DataAccessException || exception instanceof PersistenceException) {
handleDatabaseException(exception, response);
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(),
@ -217,8 +219,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException {
ThingsboardErrorResponse errorResponse;
if (databaseException instanceof ConstraintViolationException) {
errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
ConstraintViolationException constraintViolationException = DaoUtil.extractConstraintViolation(databaseException);
if (constraintViolationException != null) {
log.debug("Constraint violation: {}", ExceptionUtils.getRootCauseMessage(databaseException));
String constraintName = constraintViolationException.getConstraintName();
String userMessage;
if (constraintName != null && !constraintName.isEmpty()) {
userMessage = "Constraint violation: " + constraintName;
} else {
userMessage = "Constraint violation";
}
errorResponse = ThingsboardErrorResponse.of(userMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
} else {
log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException));
errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR);

8
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -25,6 +25,7 @@ import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.pat.ApiKeyService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
import org.thingsboard.server.service.edge.rpc.processor.apikey.ApiKeyProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -273,6 +275,12 @@ public class EdgeContextComponent {
@Autowired
private AiModelProcessor aiModelProcessor;
@Autowired
private ApiKeyService apiKeyService;
@Autowired
private ApiKeyProcessor apiKeyProcessor;
@Autowired
private UserProcessor userProcessor;

16
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset;
@ -61,6 +62,7 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -98,6 +100,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -741,6 +744,19 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
}
public static ApiKeyUpdateMsg constructApiKeyUpdatedMsg(UpdateMsgType msgType, ApiKey apiKey) {
return ApiKeyUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(apiKey))
.setIdMSB(apiKey.getId().getId().getMostSignificantBits())
.setIdLSB(apiKey.getId().getId().getLeastSignificantBits()).build();
}
public static ApiKeyUpdateMsg constructApiKeyDeleteMsg(ApiKeyId apiKeyId) {
return ApiKeyUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(apiKeyId.getId().getMostSignificantBits())
.setIdLSB(apiKeyId.getId().getLeastSignificantBits()).build();
}
public static List<EdgeEvent> mergeAndFilterDownlinkDuplicates(List<EdgeEvent> edgeEvents) {
try {
edgeEvents = removeDownlinkDuplicates(edgeEvents);

40
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
import io.grpc.stub.StreamObserver;
import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy;
@ -37,7 +40,8 @@ import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId;
@ -67,7 +71,6 @@ import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@ -112,6 +115,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private String certFileResource;
@Value("${edges.rpc.ssl.private_key}")
private String privateKeyResource;
@Value("${edges.rpc.ssl.key_password:}")
private String keyPassword;
@Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@Value("${edges.rpc.client_max_keep_alive_time_sec:1}")
@ -173,9 +178,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
.addService(this);
if (sslEnabled) {
try {
InputStream certFileIs = ResourceUtils.getInputStream(this, certFileResource);
InputStream privateKeyFileIs = ResourceUtils.getInputStream(this, privateKeyResource);
builder.useTransportSecurity(certFileIs, privateKeyFileIs);
setupSsl(builder);
} catch (Exception e) {
log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e);
throw new RuntimeException("Unable to set up SSL context!", e);
@ -196,6 +199,33 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.info("Edge RPC service initialized!");
}
/**
* Configures TLS for the Edge gRPC server.
* <p>
* Delegates PEM parsing and key management to {@link PemSslCredentials} — the same
* class used by MQTT, CoAP, and LwM2M transports — which supports:
* <ul>
* <li>Separate certificate and private key files (classic two-file setup)</li>
* <li>Combined PEM: certificate chain + private key in a single {@code cert} file
* ({@code private_key} left empty)</li>
* <li>Encrypted private keys (password supplied via {@code key_password})</li>
* </ul>
* Path resolution (for both {@code cert} and {@code private_key}) is handled by
* {@link org.thingsboard.server.common.data.ResourceUtils#getInputStream ResourceUtils}:
* absolute path → relative / working-dir → classpath → {@code classpath:} prefix.
*/
void setupSsl(NettyServerBuilder builder) throws Exception {
PemSslCredentials credentials = new PemSslCredentials();
credentials.setCertFile(certFileResource);
credentials.setKeyFile(StringUtils.isEmpty(privateKeyResource) ? null : privateKeyResource);
credentials.setKeyPassword(keyPassword);
credentials.init(false);
SslContext sslContext = GrpcSslContexts.configure(
SslContextBuilder.forServer(credentials.createKeyManagerFactory())).build();
builder.sslContext(sslContext);
}
@PreDestroy
public void destroy() {
if (server != null) {

11
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -988,6 +989,16 @@ public abstract class EdgeGrpcSession implements Closeable {
}
}
}
if (uplinkMsg.getApiKeyUpdateMsgCount() > 0) {
for (ApiKeyUpdateMsg apiKeyUpdateMsg : uplinkMsg.getApiKeyUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getApiKeyProcessor().processApiKeyMsgFromEdge(edge.getTenantId(), edge, apiKeyUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
} catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -146,7 +146,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, API_KEY, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};

105
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java

@ -0,0 +1,105 @@
/**
* 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.service.edge.rpc.processor.apikey;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class ApiKeyEdgeProcessor extends BaseApiKeyProcessor implements ApiKeyProcessor {
@Override
public ListenableFuture<Void> processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg) {
ApiKeyId apiKeyId = new ApiKeyId(new UUID(apiKeyUpdateMsg.getIdMSB(), apiKeyUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
return switch (apiKeyUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
boolean created = saveOrUpdateApiKey(tenantId, apiKeyId, apiKeyUpdateMsg);
if (created) {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (apiKey != null) {
pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_CREATED);
}
}
yield Futures.immediateFuture(null);
}
case ENTITY_DELETED_RPC_MESSAGE -> {
deleteApiKey(tenantId, edge, apiKeyId);
yield Futures.immediateFuture(null);
}
default -> handleUnsupportedMsgType(apiKeyUpdateMsg.getMsgType());
};
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
ApiKeyId apiKeyId = new ApiKeyId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(edgeEvent.getTenantId(), apiKeyId);
if (apiKey != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addApiKeyUpdateMsg(apiKeyUpdateMsg)
.build();
}
}
case DELETED -> {
ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyDeleteMsg(apiKeyId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addApiKeyUpdateMsg(apiKeyUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.API_KEY;
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java

@ -0,0 +1,28 @@
/**
* 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.service.edge.rpc.processor.apikey;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface ApiKeyProcessor extends EdgeProcessor {
ListenableFuture<Void> processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg);
}

63
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java

@ -0,0 +1,63 @@
/**
* 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.service.edge.rpc.processor.apikey;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseApiKeyProcessor extends BaseEdgeProcessor {
protected boolean saveOrUpdateApiKey(TenantId tenantId, ApiKeyId apiKeyId, ApiKeyUpdateMsg apiKeyUpdateMsg) {
boolean isCreated = false;
try {
ApiKey apiKey = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
if (apiKey == null) {
throw new RuntimeException("[{" + tenantId + "}] apiKeyUpdateMsg {" + apiKeyUpdateMsg + " } cannot be converted to apiKey");
}
ApiKey existingApiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (existingApiKey == null) {
apiKey.setCreatedTime(Uuids.unixTimestamp(apiKeyId.getId()));
isCreated = true;
}
apiKey.setId(apiKeyId);
edgeCtx.getApiKeyService().saveApiKey(tenantId, apiKey, apiKey.getValue(), false);
} catch (Exception e) {
log.error("[{}] Failed to process apiKey update msg [{}]", tenantId, apiKeyUpdateMsg, e);
throw e;
}
return isCreated;
}
protected void deleteApiKey(TenantId tenantId, Edge edge, ApiKeyId apiKeyId) {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (apiKey != null) {
edgeCtx.getApiKeyService().deleteApiKey(tenantId, apiKey, false);
pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_DELETED);
}
}
}

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java

@ -23,6 +23,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -42,6 +44,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.List;
import java.util.UUID;
@Slf4j
@ -135,6 +138,10 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso
if (userCredentialsByUserId != null) {
builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId));
}
List<ApiKey> apiKeys = edgeCtx.getApiKeyService().findApiKeysByUserId(edgeEvent.getTenantId(), userId);
for (ApiKey apiKey : apiKeys) {
builder.addApiKeyUpdateMsg(EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey));
}
return builder.build();
}
}

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.web.client.RestTemplate;
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.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
@ -109,10 +110,13 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
adaptiveCard.getActions().add(actionOpenUrl);
}
URI webhookUri = new URI(targetConfig.getWebhookUrl());
SsrfProtectionValidator.validateUri(webhookUri);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
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 {
@ -139,10 +143,13 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
teamsMessageCard.setPotentialAction(List.of(actionCard));
}
URI webhookUri = new URI(targetConfig.getWebhookUrl());
SsrfProtectionValidator.validateUri(webhookUri);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
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 {

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

2
application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java

@ -46,7 +46,7 @@ public class ApiKeyAuthenticationProvider extends AbstractAuthenticationProvider
return new ApiKeyAuthenticationToken(securityUser);
}
private SecurityUser authenticate(String key) {
public SecurityUser authenticate(String key) {
if (StringUtils.isEmpty(key)) {
throw new BadCredentialsException("Empty API key");
}

23
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -34,6 +34,7 @@ import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
@ -242,7 +243,10 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId());
log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId(), t);
if (t instanceof ResultSetSizeLimitExceededException) {
sendError(finalCtx, t);
}
}
}, wsCallBackExecutor);
}
@ -258,7 +262,18 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
handleLatestCmd(ctx, cmd.getLatestCmd());
}
if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(ctx, cmd.getTsCmd());
Futures.addCallback(handleTimeSeriesCmd(ctx, cmd.getTsCmd()), new FutureCallback<>() {
@Override
public void onSuccess(TbEntityDataSubCtx result) {}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process timeseries command", ctx.getSessionId(), ctx.getCmdId(), t);
if (t instanceof ResultSetSizeLimitExceededException) {
sendError(ctx, t);
}
}
}, wsCallBackExecutor);
}
} else {
checkAndSendInitialData(ctx);
@ -268,6 +283,10 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
}
private void sendError(TbEntityDataSubCtx ctx, Throwable t) {
ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), t.getMessage()));
}
private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) {
if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());

4
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -348,7 +348,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (sub.isLatestValues()) {
for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey());
if (stateTs == null || kv.getTs() >= stateTs) {
if (stateTs == null || kv.getTs() >= stateTs || kv.isDeletedEntry()) {
if (updateData == null) {
updateData = new ArrayList<>();
}
@ -362,7 +362,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey());
if (stateTs != null) {
if (!sub.isLatestValues() || kv.getTs() >= stateTs) {
if (!sub.isLatestValues() || kv.getTs() >= stateTs || kv.isDeletedEntry()) {
if (updateData == null) {
updateData = new ArrayList<>();
}

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 D newExportData() {
return (D) new EntityExportData<E>();
}
public abstract Set<EntityType> getSupportedEntityTypes();
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
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);
if (entity == null) {
@ -78,7 +79,6 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
}
exportData.setEntity(entity);
exportData.setEntityType(entityId.getEntityType());
setAdditionalExportData(ctx, entity, exportData);
if (entity instanceof HasVersion hasVersion) {
hasVersion.setVersion(null);
@ -223,8 +223,4 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
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
public Set<EntityType> getSupportedEntityTypes() {
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()));
}
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
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
public Set<EntityType> getSupportedEntityTypes() {
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
public Set<EntityType> getSupportedEntityTypes() {
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);
}
@Override
protected WidgetsBundleExportData newExportData() {
return new WidgetsBundleExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.WIDGETS_BUNDLE);

4
application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java

@ -32,4 +32,8 @@ public abstract class AbstractCleanUpService {
return partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition();
}
protected boolean isTenantPartitionMine(TenantId tenantId) {
return partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition();
}
}

75
application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java

@ -20,33 +20,41 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.dao.notification.NotificationRequestDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME;
@Slf4j
@Service
@ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0")
@Slf4j
public class NotificationsCleanUpService extends AbstractCleanUpService {
private final SqlPartitioningRepository partitioningRepository;
private final NotificationRequestDao notificationRequestDao;
private final TenantService tenantService;
@Value("${sql.ttl.notifications.ttl:2592000}")
private long ttlInSec;
@Value("${sql.notifications.partition_size:168}")
private int partitionSizeInHours;
@Value("${sql.ttl.notifications.removal_batch_size:10000}")
private int removalBatchSize;
public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository,
NotificationRequestDao notificationRequestDao) {
NotificationRequestDao notificationRequestDao, TenantService tenantService) {
super(partitionService);
this.partitioningRepository = partitioningRepository;
this.notificationRequestDao = notificationRequestDao;
this.tenantService = tenantService;
}
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.notifications.checking_interval_ms:86400000})}",
@ -54,18 +62,65 @@ public class NotificationsCleanUpService extends AbstractCleanUpService {
public void cleanUp() {
long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec);
long partitionDurationMs = TimeUnit.HOURS.toMillis(partitionSizeInHours);
if (!isSystemTenantPartitionMine()) {
if (isSystemTenantPartitionMine()) {
partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
} else {
partitioningRepository.cleanupPartitionsCache(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
return;
}
long lastRemovedNotificationTs = partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
if (lastRemovedNotificationTs > 0) {
long gap = TimeUnit.MINUTES.toMillis(10);
long requestExpTime = lastRemovedNotificationTs - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap;
int removed = notificationRequestDao.removeAllByCreatedTimeBefore(requestExpTime);
log.info("Removed {} outdated notification requests older than {}", removed, requestExpTime);
long gap = TimeUnit.MINUTES.toMillis(10);
long requestExpTime = expTime - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap;
cleanUpNotificationRequests(requestExpTime);
}
private void cleanUpNotificationRequests(long expirationTime) {
log.info("Starting notification requests cleanup for records older than {}", Instant.ofEpochMilli(expirationTime));
int totalRemoved = 0;
int tenantsProcessed = 0;
// Clean up SYSADMIN's notification requests on the system node only
if (isSystemTenantPartitionMine()) {
try {
totalRemoved += cleanUpByTenant(TenantId.SYS_TENANT_ID, expirationTime);
} catch (Exception e) {
log.warn("Failed to clean up notification requests for sysadmin {}", TenantId.SYS_TENANT_ID, e);
}
}
// Each node cleans up notification requests for its own tenants
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
for (TenantId tenantId : tenants) {
try {
if (!isTenantPartitionMine(tenantId)) {
continue;
}
int tenantRemoved = cleanUpByTenant(tenantId, expirationTime);
totalRemoved += tenantRemoved;
tenantsProcessed++;
if (tenantRemoved > 0) {
log.trace("Removed {} notification requests for tenant {}", tenantRemoved, tenantId);
}
} catch (Exception e) {
log.warn("Failed to clean up notification requests for tenant {}", tenantId, e);
}
}
log.info("Notification requests cleanup completed. Processed {} tenants, removed {} total records older than {}", tenantsProcessed, totalRemoved, Instant.ofEpochMilli(expirationTime));
}
private int cleanUpByTenant(TenantId tenantId, long expirationTime) {
int totalRemoved = 0;
int batchRemoved;
do {
batchRemoved = notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, removalBatchSize);
totalRemoved += batchRemoved;
if (batchRemoved > 0) {
log.trace("Removed {} notification requests in batch for tenant {}", batchRemoved, tenantId);
}
} while (batchRemoved >= removalBatchSize);
return totalRemoved;
}
}

94
application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java

@ -15,69 +15,87 @@
*/
package org.thingsboard.server.service.ttl.rpc;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.rpc.RpcDao;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ttl.AbstractCleanUpService;
import java.util.Date;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@Slf4j
@RequiredArgsConstructor
public class RpcCleanUpService {
@Value("${sql.ttl.rpc.enabled}")
private boolean ttlTaskExecutionEnabled;
@Service
@TbCoreComponent
@ConditionalOnExpression("${sql.ttl.rpc.enabled:true}")
public class RpcCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.rpc.removal_batch_size:10000}")
private int removalBatchSize;
private final RpcDao rpcDao;
private final TenantService tenantService;
private final PartitionService partitionService;
private final TbTenantProfileCache tenantProfileCache;
private final RpcDao rpcDao;
public RpcCleanUpService(TenantService tenantService, PartitionService partitionService, TbTenantProfileCache tenantProfileCache, RpcDao rpcDao) {
super(partitionService);
this.tenantService = tenantService;
this.tenantProfileCache = tenantProfileCache;
this.rpcDao = rpcDao;
}
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.rpc.checking_interval})}", fixedDelayString = "${sql.ttl.rpc.checking_interval}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
PageLink tenantsBatchRequest = new PageLink(10_000, 0);
PageData<TenantId> tenantsIds;
do {
tenantsIds = tenantService.findTenantsIds(tenantsBatchRequest);
for (TenantId tenantId : tenantsIds.getData()) {
if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
continue;
}
Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getRpcTtlDays() == 0) {
continue;
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getRpcTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
int totalRemoved = rpcDao.deleteOutdatedRpcByTenantId(tenantId, expirationTime);
if (totalRemoved > 0) {
log.info("Removed {} outdated rpc(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime));
}
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
for (TenantId tenantId : tenants) {
try {
if (!isTenantPartitionMine(tenantId)) {
continue;
}
tenantsBatchRequest = tenantsBatchRequest.nextPageLink();
} while (tenantsIds.hasNext());
Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getRpcTtlDays() == 0) {
continue;
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getRpcTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
int totalRemoved = cleanUpByTenant(tenantId, expirationTime);
if (totalRemoved > 0) {
log.info("Removed {} outdated rpc(s) for tenant {} older than {}", totalRemoved, tenantId, Instant.ofEpochMilli(expirationTime));
}
} catch (Exception e) {
log.warn("Failed to clean up rpc by ttl for tenant {}", tenantId, e);
}
}
}
private int cleanUpByTenant(TenantId tenantId, long expirationTime) {
int totalRemoved = 0;
int batchRemoved;
do {
batchRemoved = rpcDao.deleteOutdatedRpcByTenantIdBatch(tenantId, expirationTime, removalBatchSize);
totalRemoved += batchRemoved;
if (batchRemoved > 0) {
log.trace("Removed {} rpc in batch for tenant {}", batchRemoved, tenantId);
}
} while (batchRemoved >= removalBatchSize);
return totalRemoved;
}
}

3
application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java

@ -23,11 +23,14 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class AuthCmd implements WsCmd {
private int cmdId;
private String token;
private String apiKey;
@Override
public WsCmdType getType() {
return WsCmdType.AUTH;
}
}

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

@ -15,6 +15,7 @@
#
# Server common parameters
# Configures HTTP/HTTPS bind address, port, SSL, WebSocket, and REST API settings.
server:
# Server bind-address
address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
@ -109,23 +110,25 @@ server:
response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}"
# Application info parameters
# Exposes application metadata such as version string injected at build time.
app:
# Application version
version: "@project.version@"
# Zookeeper connection parameters
# ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk:
# Enable/disable zookeeper discovery service.
# Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string
# ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds
# ZooKeeper retry interval in milliseconds
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}"
# Zookeeper session timeout in milliseconds
# ZooKeeper session timeout in milliseconds
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}"
# 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.
@ -133,6 +136,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cluster parameters
# Controls cluster statistics — tracks the number of messages exchanged between cluster nodes.
cluster:
stats:
# 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}"
# Plugins configuration parameters
# Defines classpath scan packages used to discover and register ThingsBoard extension plugins.
plugins:
# Comma-separated package list used during classpath scanning for plugins
scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
# Security parameters
# Configures JWT tokens, user login, device claiming, OAuth2, and CA certificate trust store.
security:
# 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
@ -182,8 +188,60 @@ security:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}"
# The password of the cacerts keystore file
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
# Configures mail service OAuth2 token refresh and per-tenant sending rate limits.
mail:
oauth2:
# 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:}"
# Usage statistics parameters
# Controls collection and reporting intervals for API usage stats at system, tenant, and customer levels.
usage:
stats:
report:
@ -215,6 +274,7 @@ usage:
report_interval: "${DEVICES_STATS_REPORT_INTERVAL:60}"
# UI settings parameters
# Configures dashboard data limits and base URL for UI help assets.
ui:
# Dashboard parameters
dashboard:
@ -226,6 +286,7 @@ ui:
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}"
# Database telemetry parameters
# Selects the storage backend (SQL, Cassandra, or TimescaleDB) for time-series and latest telemetry data.
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:
@ -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)
# Cassandra driver configuration parameters
# Configures cluster name, keyspace, SSL, credentials, socket, and query settings for the Cassandra driver.
cassandra:
# Thingsboard cluster name
cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}"
@ -352,6 +414,7 @@ cassandra:
print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}"
# SQL configuration parameters
# Tunes batch sizes, delays, thread counts, TTL, and partitioning for SQL-backed persistence of telemetry, events, and audit logs.
sql:
# Specify batch size for persisting attribute updates
attributes:
@ -432,10 +495,11 @@ sql:
edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month
alarms:
checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # To delete outdated alarms not all at once but in batches
removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # Batch size for records removal
rpc:
enabled: "${SQL_TTL_RPC_ENABLED:true}" # Enable/disable TTL (Time To Live) for rpc call records
checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
removal_batch_size: "${SQL_RPC_TTL_REMOVAL_BATCH_SIZE:10000}" # Batch size for records removal
audit_logs:
enabled: "${SQL_TTL_AUDIT_LOGS_ENABLED:true}" # Enable/disable TTL (Time To Live) for audit log records
ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. The accuracy of the cleanup depends on the sql.audit_logs.partition_size
@ -444,6 +508,7 @@ sql:
enabled: "${SQL_TTL_NOTIFICATIONS_ENABLED:true}" # Enable/disable TTL (Time To Live) for notification center records
ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days
checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
removal_batch_size: "${SQL_TTL_NOTIFICATIONS_REMOVAL_BATCH_SIZE:10000}" # Batch size for records removal
api_keys:
enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for expired api keys records
checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
@ -453,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
# Actor system parameters
# Configures thread pools, timeouts, and behavior for the internal actor system processing devices, rules, and RPCs.
actors:
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
@ -521,6 +587,10 @@ actors:
# Comma-separated list of additional blocked destinations (IPs, CIDR subnets, or hostnames).
# Example: "198.51.100.0/24,metadata.tencentyun.com,rancher-metadata"
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:
# Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
@ -553,6 +623,8 @@ actors:
# Time in seconds to receive calculation result.
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:
settings:
# Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one.
@ -560,6 +632,7 @@ debug:
default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}"
# Cache settings parameters
# Configures cache type (Caffeine or Redis), pool size, and per-entity TTL and max-size limits for all caches.
cache:
# caffeine or redis(7.2 - latest compatible version)
type: "${CACHE_TYPE:caffeine}"
@ -719,6 +792,7 @@ cache:
spring.data.redis.repositories.enabled: false # Disable this because it is not required.
# Redis/Valkey configuration parameters
# Configures standalone, cluster, or sentinel Redis connection, SSL, and connection pool settings used for caching.
redis:
# standalone or cluster or sentinel
connection:
@ -755,9 +829,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}"
# If set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index
# Redis logical database index to select after connecting.
db: "${REDIS_DB:0}"
# db password
# Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}"
@ -802,27 +876,36 @@ redis:
# Update version parameters
# Controls whether the platform periodically checks for new ThingsBoard releases.
updates:
# Enable/disable checks for the new version
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:
mappings:
# Intercept path
"[/api/**]":
#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.
allowed-methods: "*"
allowed-methods: "${TB_CORS_ALLOWED_METHODS:*}"
#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.
max-age: "1800"
max-age: "${TB_CORS_MAX_AGE:1800}"
#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
# 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.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
@ -841,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)
# SQL DAO Configuration parameters
# Configures Spring JPA, Hibernate DDL, and HikariCP datasource connections for the primary and events databases.
spring:
data:
jpa:
@ -891,6 +975,7 @@ spring:
registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"
# Audit log parameters
# Configures audit logging levels per entity type and optional forwarding to an external sink such as Elasticsearch.
audit-log:
# Enable/disable audit log functionality.
enabled: "${AUDIT_LOG_ENABLED:true}"
@ -933,6 +1018,7 @@ audit-log:
password: "${AUDIT_LOG_SINK_PASSWORD:}" # Password used to access external sink system
# Device state parameters
# Configures device inactivity detection, state persistence strategy, and rate limits for state rule nodes.
state:
# 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).
@ -963,7 +1049,8 @@ state:
# 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}"
# Tbel parameters
# TBEL parameters
# Configures the ThingsBoard Expression Language (TBEL) engine: limits, timeouts, thread pool, and blacklisting.
tbel:
# Enable/Disable TBEL feature.
enabled: "${TBEL_ENABLED:true}"
@ -992,6 +1079,7 @@ tbel:
print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}"
# JS parameters
# Configures the JavaScript execution engine (local Nashorn or remote Node.js): limits, sandboxing, and thread pools.
js:
# local (Nashorn Engine, deprecated) OR remote JS-Executors (NodeJS)
evaluator: "${JS_EVALUATOR:local}"
@ -1039,6 +1127,7 @@ js:
print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}"
# Transport configuration parameters
# Configures session management, rate limits, and protocol-specific settings for HTTP, MQTT, CoAP, LwM2M, and SNMP transports.
transport:
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.
@ -1349,6 +1438,7 @@ transport:
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
# CoAP server parameters
# Configures the standalone CoAP server bind address, port, DTLS encryption, and credential settings.
coap:
server:
# Enable/disable coap server.
@ -1433,6 +1523,7 @@ coap:
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
# Device connectivity parameters
# Specifies the hosts, ports, and credentials exposed to the UI for generating device connection check commands.
device:
connectivity:
http:
@ -1486,6 +1577,7 @@ device:
image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}"
# Edges parameters
# Controls Edge instance gRPC communication, event storage, state persistence, and statistics reporting.
edges:
# Enable/disable Edge instance
enabled: "${EDGES_ENABLED:true}"
@ -1507,10 +1599,17 @@ edges:
ssl:
# Enable/disable SSL support
enabled: "${EDGES_RPC_SSL_ENABLED:false}"
# Cert file to be used during TLS connectivity to the cloud
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key).
# Accepts an absolute filesystem path (e.g. /etc/thingsboard/certChainFile.pem),
# a relative path resolved against the working directory first then the classpath,
# or a classpath resource with the explicit "classpath:" prefix (e.g. classpath:conf/certChainFile.pem).
cert: "${EDGES_RPC_SSL_CERT:certChainFile.pem}"
# Private key file associated with the Cert certificate. This key is used in the encryption process during a secure connection
# Path to the server certificate private key file. Optional if the private key is already present in the cert file above.
# Supports the same path resolution as 'cert': absolute, relative/classpath, or "classpath:" prefix.
# Leave empty when using a combined PEM cert that already contains the private key.
private_key: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}"
# Server certificate private key password (optional). Leave empty if the key is not encrypted.
key_password: "${EDGES_RPC_SSL_KEY_PASSWORD:}"
# Maximum size (in bytes) of inbound messages the cloud can handle from the edge. By default, it can handle messages up to 4 Megabytes
max_inbound_message_size: "${EDGES_RPC_MAX_INBOUND_MESSAGE_SIZE:4194304}"
# Maximum length of telemetry (time-series and attributes) message the cloud sends to the edge. By default, there is no limitation.
@ -1547,6 +1646,7 @@ edges:
report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}"
# Spring doc common parameters
# Enables or disables the OpenAPI/Swagger documentation endpoint and sets the default media type.
springdoc:
# If false swagger API docs will be unavailable
api-docs.enabled: "${SWAGGER_ENABLED:true}"
@ -1554,6 +1654,7 @@ springdoc:
default-produces-media-type: "${SWAGGER_DEFAULT_PRODUCES_MEDIA_TYPE:application/json}"
# Swagger common parameters
# Configures Swagger UI metadata: title, description, contact, license, version, and API path patterns.
swagger:
# General swagger match pattern of swagger UI links
api_path: "${SWAGGER_API_PATH:/api/**}"
@ -1585,6 +1686,7 @@ swagger:
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters
# Configures the message queue backend (in-memory or Kafka) and all topic/partition/consumer settings for each service.
queue:
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).
@ -1998,12 +2100,14 @@ queue:
processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}"
# Event configuration parameters
# Controls limits on debug event content size stored for rule chain and rule node execution.
event:
debug:
# Maximum number of symbols per debug event. The event content will be truncated if needed
max-symbols: "${TB_MAX_DEBUG_EVENT_SYMBOLS:4096}"
# General service parameters
# Sets the deployment type (monolith or microservice) and assigns tenant profiles to specific Rule Engine instances.
service:
type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine
# Unique id for this service (autogenerated if empty)
@ -2017,6 +2121,7 @@ service:
executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}"
# Metrics parameters
# Enables actuator metrics endpoint and configures system info (CPU, memory) persistence frequency and TTL.
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"
@ -2030,6 +2135,7 @@ metrics:
ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}"
# Version control parameters
# Configures thread pools and Git repository folder for entity version control (export/import) operations.
vc:
# Pool size for handling export tasks
thread_pool_size: "${TB_VC_POOL_SIZE:6}"
@ -2040,6 +2146,7 @@ vc:
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Notification system parameters
# Configures thread pool size and deduplication intervals for the notification rule processing engine.
notification_system:
# 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}"
@ -2048,6 +2155,7 @@ notification_system:
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}"
# General management parameters
# Configures Spring Boot Actuator endpoint exposure and health indicator settings.
management:
endpoints:
web:
@ -2060,6 +2168,7 @@ management:
enabled: "false"
# 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:
# Server domain name for Thingsboard Live mobile application
domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}"
@ -2068,6 +2177,8 @@ mobileApp:
# 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}"
# MQTT client parameters
# Configures MQTT client retransmission behavior including max attempts, initial delay, and jitter factor.
mqtt:
# MQTT client configuration parameters
client:

11
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -86,12 +86,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
}
@Before
public void beforeWsTest() throws Exception {
public void beforeWsTest() {
// placeholder
}
@After
public void afterWsTest() throws Exception {
public void afterWsTest() {
if (wsClient != null) {
wsClient.close();
}
@ -113,4 +113,11 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
return wsClient;
}
protected TbTestWebSocketClient buildAndConnectWebSocketClientWithApiKey(String apiKey) throws URISyntaxException, InterruptedException {
TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"));
assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
wsClient.authenticateWithApiKey(apiKey);
return wsClient;
}
}

2
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -1162,7 +1162,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
assertThat(findRelationsByTo(entityTo)).hasSize(1);
doDelete(urlDelete)
.andExpect(status().isInternalServerError());
.andExpect(status().isBadRequest());
assertThat(findRelationsByTo(entityTo)).hasSize(1);
} finally {

71
application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java

@ -0,0 +1,71 @@
/**
* 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.controller;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class ApiKeyWebSocketApiTest extends WebSocketApiTest {
private ApiKey apiKey;
@Before
public void setUpApiKey() {
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setDescription("WS test API key");
apiKeyInfo.setEnabled(true);
apiKeyInfo.setUserId(tenantAdminUserId);
apiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class);
}
@After
public void tearDownApiKey() throws Exception {
loginTenantAdmin();
doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk());
}
@Override
protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
return buildAndConnectWebSocketClientWithApiKey(apiKey.getValue());
}
@Test
public void testInvalidApiKeyAuthCmd_connectionClosed() throws Exception {
TbTestWebSocketClient client = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"));
assertThat(client.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
try {
client.authenticateWithApiKey("invalid-key");
assertThat(client.waitForClose()).isTrue();
} finally {
client.close();
}
}
}

33
application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java

@ -62,6 +62,9 @@ import static org.mockito.Mockito.verify;
@DaoSqlTest
public class AuditLogControllerTest extends AbstractControllerTest {
// Small enough to force multiple pages, verifying pagination loop correctness
private static final int SMALL_PAGE_SIZE = 5;
private Tenant savedTenant;
private User tenantAdmin;
@ -113,17 +116,14 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/device", device, Device.class);
}
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?");
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1));
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1));
}
@Test
@ -138,7 +138,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
}
int expectedSize = loadedAuditLogsBefore.size() + 3;
Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test",
expectedSize, getAuditLogs(100, "/api/audit/logs?").size()));
}
@ -170,9 +170,9 @@ public class AuditLogControllerTest extends AbstractControllerTest {
savedDevice = doPost("/api/device", savedDevice, Device.class);
}
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
Device finalSavedDevice = savedDevice;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1));
}
@Test
@ -188,9 +188,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
tenantProfile.setName(tenantProfile.getName() + "(old)");
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?");
Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size());
TenantProfile finalTenantProfile = tenantProfile;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + finalTenantProfile.getId().getEntityType() + "/" + finalTenantProfile.getId().getId() + "?"))
.as("Audit logs count by Tenant Profile entity").hasSize(2));
//cleanup
doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString());

4
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
@ -108,6 +109,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@ -115,6 +117,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
@ -322,6 +325,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType(cfType);
calculatedField.setName("Test Calculated Field for " + entityId);
calculatedField.setConfigurationVersion(1);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
if (customConfiguration != null) {
calculatedField.setConfiguration(customConfiguration);
} else switch (cfType) {

12
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -41,6 +41,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.TestSocketUtils;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
@ -90,8 +91,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_URL;
@TestPropertySource(properties = {
"transport.mqtt.enabled=true",
@ -101,6 +100,15 @@ import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.
@ContextConfiguration(classes = {EntityViewControllerTest.Config.class})
@DaoSqlTest
public class EntityViewControllerTest extends AbstractControllerTest {
// Must NOT be imported from AbstractMqttIntegrationTest. That field is a static final initialized
// once per JVM. Other test classes (e.g. MqttGatewayRateLimitsTest, DeviceEdgeTest) share the same
// constant but produce a different Spring context cache key, so Spring creates a separate
// ApplicationContext for each of them. Every context starts its own MqttTransportService and tries
// to bind the same port, causing BindException when tests run in the same Surefire JVM fork.
// Declaring the port here gives this context its own independently allocated port.
static final int MQTT_PORT = TestSocketUtils.findAvailableTcpPort();
static final String MQTT_URL = "tcp://localhost:" + MQTT_PORT;
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("transport.mqtt.bind_port = {}", MQTT_PORT);

24
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -39,7 +39,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.net.URI;
import java.nio.channels.NotYetConnectedException;
@ -53,6 +52,8 @@ public class TbTestWebSocketClient extends WebSocketClient {
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30);
private final CountDownLatch closeLatch = new CountDownLatch(1);
@Getter
private volatile String lastMsg;
private volatile CountDownLatch reply;
@ -62,6 +63,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
@ -69,7 +71,13 @@ public class TbTestWebSocketClient extends WebSocketClient {
public void authenticate(String token) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setAuthCmd(new AuthCmd(1, token));
cmdsWrapper.setAuthCmd(new AuthCmd(1, token, null));
send(JacksonUtil.toString(cmdsWrapper));
}
public void authenticateWithApiKey(String apiKey) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setAuthCmd(new AuthCmd(1, null, apiKey));
send(JacksonUtil.toString(cmdsWrapper));
}
@ -88,6 +96,16 @@ public class TbTestWebSocketClient extends WebSocketClient {
@Override
public void onClose(int i, String s, boolean b) {
log.info("CLOSED.");
closeLatch.countDown();
}
public boolean waitForClose() {
try {
return closeLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.warn("Failed to await close", e);
return false;
}
}
@Override
@ -275,7 +293,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) {
log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope);
TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null);
TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null);
cmd.setEntityId(entityId.getId().toString());
cmd.setEntityType(entityId.getEntityType().toString());
cmd.setCmdId(1);

21
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -936,6 +937,26 @@ public class TenantControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService);
}
@Test
public void testSaveTenantWithNonExistentTenantProfileId() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
tenant.setTenantProfileId(new TenantProfileId(UUID.randomUUID()));
String responseBody = doPost("/api/tenant", tenant)
.andExpect(status().isBadRequest())
.andReturn().getResponse().getContentAsString();
// Verify sanitized message format
assertThat(responseBody).contains("Constraint violation: fk_tenant_profile");
// Verify raw SQL details are not returned
assertThat(responseBody).doesNotContain("could not execute statement");
assertThat(responseBody).doesNotContain("insert or update on table");
assertThat(responseBody).doesNotContain("tenant_profile_id");
assertThat(responseBody).doesNotContain("is not present in table");
}
private void testBroadcastEntityStateChangeEventNeverTenant() {
Mockito.verify(tbClusterService, never()).onTenantChange(Mockito.any(Tenant.class),
Mockito.isNull());

266
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java → application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java

@ -19,13 +19,16 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
@ -60,7 +63,9 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@ -71,13 +76,11 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -91,10 +94,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"server.ws.alarms_per_alarm_status_subscription_cache_size=5",
"server.ws.dynamic_page_link.refresh_interval=15"
})
public class WebsocketApiTest extends AbstractControllerTest {
public class WebSocketApiTest extends AbstractControllerTest {
@Autowired
private TelemetrySubscriptionService tsService;
@SpyBean
private TimeseriesService timeseriesService;
Device device;
DeviceTypeFilter dtf;
@ -128,8 +135,8 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length);
Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertEquals(0, pageData.getData().getFirst().getTimeseries().get("temperature").length);
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L));
@ -144,8 +151,8 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> dataList = update.getUpdate();
Assert.assertNotNull(dataList);
Assert.assertEquals(1, dataList.size());
Assert.assertEquals(device.getId(), dataList.get(0).getEntityId());
TsValue[] tsArray = dataList.get(0).getTimeseries().get("temperature");
Assert.assertEquals(device.getId(), dataList.getFirst().getEntityId());
TsValue[] tsArray = dataList.getFirst().getTimeseries().get("temperature");
Assert.assertEquals(3, tsArray.length);
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]);
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]);
@ -162,7 +169,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 43L));
@ -176,8 +183,8 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId());
TsValue[] tsArray = listData.get(0).getTimeseries().get("temperature");
Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
TsValue[] tsArray = listData.getFirst().getTimeseries().get("temperature");
Assert.assertEquals(3, tsArray.length);
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]);
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]);
@ -186,7 +193,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
now = System.currentTimeMillis();
TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L));
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint4));
sendTelemetry(device, List.of(dataPoint4));
String msg = getWsClient().waitForUpdate();
update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
@ -194,9 +201,9 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getTimeseries());
TsValue[] tsValues = eData.get(0).getTimeseries().get("temperature");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getTimeseries());
TsValue[] tsValues = eData.getFirst().getTimeseries().get("temperature");
Assert.assertNotNull(tsValues);
Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]);
}
@ -521,7 +528,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
// check device
// check a device
AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL));
getWsClient().send(deviceCmd);
@ -589,13 +596,13 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
List<TsKvEntry> tsData = Arrays.asList(dataPoint1);
List<TsKvEntry> tsData = List.of(dataPoint1);
sendTelemetry(device, tsData);
update = getWsClient().subscribeLatestUpdate(keys);
@ -605,36 +612,36 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue);
now = System.currentTimeMillis();
TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L));
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2));
sendTelemetry(device, List.of(dataPoint2));
update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
//Sending update from the past, while latest value has new timestamp;
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint1));
sendTelemetry(device, List.of(dataPoint1));
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
//Sending duplicate update again
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2));
sendTelemetry(device, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
}
@ -677,14 +684,14 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
getWsClient().registerWaitForUpdate();
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
List<TsKvEntry> tsData = Arrays.asList(dataPoint1);
List<TsKvEntry> tsData = List.of(dataPoint1);
sendTelemetry(device, tsData);
update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
@ -694,38 +701,73 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue);
now = System.currentTimeMillis();
TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L));
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2));
sendTelemetry(device, List.of(dataPoint2));
update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
//Sending update from the past, while latest value has new timestamp;
//Sending update from the past, while the latest value has new timestamp;
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint1));
sendTelemetry(device, List.of(dataPoint1));
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
//Sending duplicate update again
getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2));
sendTelemetry(device, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
}
@Test
public void testShouldSendWsUpdateMessageWhenTelemetryWasDeleted() throws Exception {
long now = System.currentTimeMillis() - 100;
TsKvEntry dataPoint = new BasicTsKvEntry(now, new LongDataEntry("temperature", 42L));
List<TsKvEntry> tsData = List.of(dataPoint);
sendTelemetry(device, tsData);
List<EntityKey> keys = List.of(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
EntityDataUpdate update = getWsClient().subscribeLatestUpdate(keys, dtf);
Assert.assertEquals(1, update.getCmdId());
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(now, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("42", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
// delete telemetry
getWsClient().registerWaitForUpdate();
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=temperature&deleteAllDataForKeys=true", String.class);
update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(0, ""), tsValue);
}
@Test
public void testEntityDataLatestAttrWsCmd() throws Exception {
long now = System.currentTimeMillis();
@ -736,21 +778,21 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue());
Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue());
getWsClient().registerWaitForUpdate();
// Pushing update with wrong scope and make sure it will not arrive.
// Pushing update with the wrong scope and make sure it will not arrive.
AttributeKvEntry invalidDataPoint = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 55L));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(invalidDataPoint));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(invalidDataPoint));
Assert.assertNull(getWsClient().waitForUpdate(3000));
AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L));
List<AttributeKvEntry> tsData = Arrays.asList(dataPoint1);
List<AttributeKvEntry> tsData = List.of(dataPoint1);
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData);
String msg = getWsClient().waitForUpdate();
@ -761,16 +803,16 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), tsValue);
now = System.currentTimeMillis();
AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("serverAttributeKey", 52L));
getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2));
msg = getWsClient().waitForUpdate();
Assert.assertNotNull(msg);
@ -779,20 +821,20 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
tsValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
tsValue = eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), tsValue);
//Sending update from the past, while latest value has new timestamp;
//Sending update from the past, while the latest value has new timestamp;
getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint1));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint1));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
//Sending duplicate update again
getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
}
@ -812,23 +854,23 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue());
Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"));
Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue());
getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L));
List<AttributeKvEntry> tsData = Arrays.asList(dataPoint1);
List<AttributeKvEntry> tsData = List.of(dataPoint1);
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData);
@ -839,78 +881,78 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
TsValue attrValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
TsValue attrValue = eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), attrValue);
//Sending update from the past, while latest value has new timestamp;
//Sending update from the past, while the latest value has new timestamp;
getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint1));
sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint1));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
//Sending duplicate update again
getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint1));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint1));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
//Sending update from the past, while latest value has new timestamp;
getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("sharedAttributeKey", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint2));
sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE));
attrValue = eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey");
Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), attrValue);
getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint3 = new BaseAttributeKvEntry(now, new LongDataEntry("clientAttributeKey", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint3));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint3));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE));
attrValue = eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey");
Assert.assertEquals(new TsValue(dataPoint3.getLastUpdateTs(), dataPoint3.getValueAsString()), attrValue);
getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint4 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint4));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint4));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE));
attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey");
Assert.assertEquals(new TsValue(dataPoint4.getLastUpdateTs(), dataPoint4.getValueAsString()), attrValue);
getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint5 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 43L));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint5));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint5));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey");
Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE));
attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey");
Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue);
}
@ -965,13 +1007,41 @@ public class WebsocketApiTest extends AbstractControllerTest {
}
@Test
public void testHistoryCmdSendsWsErrorOnResultSetSizeLimitExceeded() throws Exception {
ResultSetSizeLimitExceededException exception = new ResultSetSizeLimitExceededException(100L, 200L);
Mockito.doReturn(Futures.immediateFailedFuture(exception))
.when(timeseriesService).findAllByQueries(Mockito.any(), Mockito.any(), Mockito.any());
List<String> keys = List.of("temperature");
long now = System.currentTimeMillis();
EntityDataUpdate errorUpdate = getWsClient().sendHistoryCmd(keys, now, TimeUnit.HOURS.toMillis(1), dtf);
assertThat(errorUpdate.getErrorCode()).isEqualTo(SubscriptionErrorCode.INTERNAL_ERROR.getCode());
assertThat(errorUpdate.getErrorMsg()).isEqualTo(exception.getMessage());
}
@Test
public void testTimeSeriesCmdSendsWsErrorOnResultSetSizeLimitExceeded() throws Exception {
ResultSetSizeLimitExceededException exception = new ResultSetSizeLimitExceededException(100L, 200L);
Mockito.doReturn(Futures.immediateFailedFuture(exception))
.when(timeseriesService).findAllByQueries(Mockito.any(), Mockito.any(), Mockito.any());
List<String> keys = List.of("temperature");
long now = System.currentTimeMillis();
EntityDataUpdate errorUpdate = getWsClient().subscribeTsUpdate(keys, now, TimeUnit.HOURS.toMillis(1), dtf);
assertThat(errorUpdate.getErrorCode()).isEqualTo(SubscriptionErrorCode.INTERNAL_ERROR.getCode());
assertThat(errorUpdate.getErrorMsg()).isEqualTo(exception.getMessage());
}
private void sendTelemetry(Device device, List<TsKvEntry> tsData) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
tsService.saveTimeseries(TimeseriesSaveRequest.builder()
.tenantId(device.getTenantId())
.entityId(device.getId())
.entries(tsData)
.callback(new FutureCallback<Void>() {
.callback(new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void result) {
log.debug("sendTelemetry callback onSuccess");

106
application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java

@ -25,16 +25,25 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketSessionType;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
@ -184,4 +193,101 @@ class TbWebSocketHandlerTest {
assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100);
}
private AuthTestFixture createAuthTestFixture() throws IOException {
TbWebSocketHandler handler = spy(new TbWebSocketHandler());
willDoNothing().given(handler).close(any(), any());
ApiKeyAuthenticationProvider apiKeyProvider = mock(ApiKeyAuthenticationProvider.class);
JwtAuthenticationProvider jwtProvider = mock(JwtAuthenticationProvider.class);
WebSocketService wsService = mock(WebSocketService.class);
ReflectionTestUtils.setField(handler, "apiKeyAuthenticationProvider", apiKeyProvider);
ReflectionTestUtils.setField(handler, "authenticationProvider", jwtProvider);
ReflectionTestUtils.setField(handler, "webSocketService", wsService);
ReflectionTestUtils.setField(handler, "tenantProfileCache", mock(TbTenantProfileCache.class));
ReflectionTestUtils.setField(handler, "authTimeoutMs", 10000);
ReflectionTestUtils.invokeMethod(handler, "init");
WebSocketSessionRef ref = WebSocketSessionRef.builder()
.sessionId(UUID.randomUUID().toString())
.sessionType(WebSocketSessionType.GENERAL)
.build();
NativeWebSocketSession wsSession = mock(NativeWebSocketSession.class);
Session nativeSess = mock(Session.class);
willReturn(nativeSess).given(wsSession).getNativeSession(Session.class);
RemoteEndpoint.Async async = mock(RemoteEndpoint.Async.class);
willReturn(async).given(nativeSess).getAsyncRemote();
willReturn("test-session-id").given(wsSession).getId();
TbWebSocketHandler.SessionMetaData sessionMd = handler.new SessionMetaData(wsSession, ref);
return new AuthTestFixture(handler, apiKeyProvider, jwtProvider, ref, sessionMd);
}
@Test
void processMsg_authenticatesWithApiKey() throws Exception {
AuthTestFixture f = createAuthTestFixture();
SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS);
willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key");
String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\"},\"cmds\":[]}";
f.handler.processMsg(f.sessionMd, msg);
verify(f.apiKeyProvider).authenticate("my-api-key");
verify(f.jwtProvider, never()).authenticate(anyString());
assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser);
}
@Test
void processMsg_authenticatesWithJwtToken() throws Exception {
AuthTestFixture f = createAuthTestFixture();
SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS);
willReturn(securityUser).given(f.jwtProvider).authenticate("my-jwt-token");
String msg = "{\"authCmd\":{\"cmdId\":1,\"token\":\"my-jwt-token\"},\"cmds\":[]}";
f.handler.processMsg(f.sessionMd, msg);
verify(f.jwtProvider).authenticate("my-jwt-token");
verify(f.apiKeyProvider, never()).authenticate(anyString());
assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser);
}
@Test
void processMsg_apiKeyTakesPrecedenceOverToken() throws Exception {
AuthTestFixture f = createAuthTestFixture();
SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS);
willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key");
String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\",\"token\":\"my-jwt-token\"},\"cmds\":[]}";
f.handler.processMsg(f.sessionMd, msg);
verify(f.apiKeyProvider).authenticate("my-api-key");
verify(f.jwtProvider, never()).authenticate(anyString());
assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser);
}
@Test
void extractQueryParam_parsesCorrectly() throws Exception {
TbWebSocketHandler handler = new TbWebSocketHandler();
Method method = TbWebSocketHandler.class.getDeclaredMethod("extractQueryParam", String.class, String.class);
method.setAccessible(true);
assertThat(method.invoke(handler, "token=jwt123", "token")).isEqualTo("jwt123");
assertThat(method.invoke(handler, "token=jwt123&other=abc123", "token")).isEqualTo("jwt123");
assertThat(method.invoke(handler, "other=value", "token")).isNull();
assertThat(method.invoke(handler, "tokenExtra=value", "token")).isNull();
}
private record AuthTestFixture(
TbWebSocketHandler handler,
ApiKeyAuthenticationProvider apiKeyProvider,
JwtAuthenticationProvider jwtProvider,
WebSocketSessionRef ref,
TbWebSocketHandler.SessionMetaData sessionMd
) {}
}

238
application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java

@ -0,0 +1,238 @@
/**
* 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.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.pat.ApiKeyService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE;
@DaoSqlTest
public class ApiKeyEdgeTest extends AbstractEdgeTest {
@Autowired
private ApiKeyService apiKeyService;
private static final String DEFAULT_API_KEY_DESCRIPTION = "Edge Test ApiKey";
private static final String UPDATED_API_KEY_DESCRIPTION = "Updated Edge Test ApiKey";
@Test
public void testApiKey_create_update_delete_fromCloud() throws Exception {
// create ApiKey
ApiKeyInfo apiKeyInfo = createSimpleApiKeyInfo(DEFAULT_API_KEY_DESCRIPTION);
edgeImitator.expectMessageAmount(1);
ApiKey savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg);
ApiKeyUpdateMsg apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
Assert.assertEquals(savedApiKey.getUuidId().getMostSignificantBits(), apiKeyUpdateMsg.getIdMSB());
Assert.assertEquals(savedApiKey.getUuidId().getLeastSignificantBits(), apiKeyUpdateMsg.getIdLSB());
ApiKey apiKeyFromMsg = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
Assert.assertNotNull(apiKeyFromMsg);
Assert.assertEquals(DEFAULT_API_KEY_DESCRIPTION, apiKeyFromMsg.getDescription());
Assert.assertEquals(savedApiKey.getTenantId(), apiKeyFromMsg.getTenantId());
// update ApiKey
edgeImitator.expectMessageAmount(1);
savedApiKey.setDescription(UPDATED_API_KEY_DESCRIPTION);
savedApiKey = doPost("/api/apiKey", new ApiKeyInfo(savedApiKey), ApiKey.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg);
apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage;
apiKeyFromMsg = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
Assert.assertNotNull(apiKeyFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_API_KEY_DESCRIPTION, apiKeyFromMsg.getDescription());
// delete ApiKey
edgeImitator.expectMessageAmount(1);
doDelete("/api/apiKey/" + savedApiKey.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg);
apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
Assert.assertEquals(savedApiKey.getUuidId().getMostSignificantBits(), apiKeyUpdateMsg.getIdMSB());
Assert.assertEquals(savedApiKey.getUuidId().getLeastSignificantBits(), apiKeyUpdateMsg.getIdLSB());
}
@Test
public void testApiKey_create_update_delete_toCloud() throws Exception {
// create
ApiKey apiKey = createSimpleApiKey(DEFAULT_API_KEY_DESCRIPTION);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, apiKey, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkApiKeyOnCloud(uplinkMsg, uuid, apiKey.getDescription());
// update
apiKey.setDescription(UPDATED_API_KEY_DESCRIPTION);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, apiKey, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkApiKeyOnCloud(updatedUplinkMsg, uuid, apiKey.getDescription());
// delete
UplinkMsg deleteUplinkMsg = getDeleteUplinkMsg(uuid);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(deleteUplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
ApiKeyId apiKeyId = new ApiKeyId(uuid);
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertNull(apiKeyService.findApiKeyById(tenantId, apiKeyId))
);
}
@Test
public void testApiKey_pushedDuringUserSync() throws Exception {
// create tenant admin user - expect 3 messages: 1 UserUpdateMsg + 2 UserCredentialsUpdateMsg
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("apiKeyTestUser@thingsboard.org");
user.setFirstName("ApiKey");
user.setLastName("TestUser");
edgeImitator.expectMessageAmount(3);
User savedUser = createUser(user, "tenant");
Assert.assertTrue(edgeImitator.waitForMessages());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
// create API key for this user - expect 1 ApiKeyUpdateMsg
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setTenantId(tenantId);
apiKeyInfo.setUserId(savedUser.getId());
apiKeyInfo.setDescription("Test API Key for user sync");
apiKeyInfo.setEnabled(true);
edgeImitator.expectMessageAmount(1);
doPost("/api/apiKey", apiKeyInfo, ApiKey.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(ApiKeyUpdateMsg.class).size());
// update user - expect 3 messages: UserUpdateMsg + UserCredentialsUpdateMsg + ApiKeyUpdateMsg
savedUser.setLastName("UpdatedLastName");
edgeImitator.expectMessageAmount(3);
doPost("/api/user", savedUser, User.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(ApiKeyUpdateMsg.class).size());
Optional<ApiKeyUpdateMsg> apiKeyUpdateMsgOpt = edgeImitator.findMessageByType(ApiKeyUpdateMsg.class);
Assert.assertTrue(apiKeyUpdateMsgOpt.isPresent());
ApiKeyUpdateMsg apiKeyUpdateMsg = apiKeyUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
}
private ApiKeyInfo createSimpleApiKeyInfo(String description) {
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setTenantId(tenantId);
apiKeyInfo.setUserId(tenantAdminUserId);
apiKeyInfo.setDescription(description);
apiKeyInfo.setEnabled(true);
return apiKeyInfo;
}
private ApiKey createSimpleApiKey(String description) {
ApiKey apiKey = new ApiKey();
apiKey.setTenantId(tenantId);
apiKey.setUserId(tenantAdminUserId);
apiKey.setDescription(description);
apiKey.setEnabled(true);
apiKey.setValue("test-api-key-value-" + UUID.randomUUID());
return apiKey;
}
private UplinkMsg getDeleteUplinkMsg(UUID uuid) throws InvalidProtocolBufferException {
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
ApiKeyUpdateMsg.Builder apiKeyDeleteMsgBuilder = ApiKeyUpdateMsg.newBuilder();
apiKeyDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE);
apiKeyDeleteMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
apiKeyDeleteMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(apiKeyDeleteMsgBuilder);
upLinkMsgBuilder.addApiKeyUpdateMsg(apiKeyDeleteMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder);
return upLinkMsgBuilder.build();
}
private UplinkMsg getUplinkMsg(UUID uuid, ApiKey apiKey, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
ApiKeyUpdateMsg.Builder apiKeyUpdateMsgBuilder = ApiKeyUpdateMsg.newBuilder();
apiKeyUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
apiKeyUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
apiKeyUpdateMsgBuilder.setEntity(JacksonUtil.toString(apiKey));
apiKeyUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(apiKeyUpdateMsgBuilder);
uplinkMsgBuilder.addApiKeyUpdateMsg(apiKeyUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkApiKeyOnCloud(UplinkMsg uplinkMsg, UUID uuid, String description) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
ApiKey apiKey = apiKeyService.findApiKeyById(tenantId, new ApiKeyId(uuid));
Assert.assertNotNull(apiKey);
Assert.assertEquals(description, apiKey.getDescription());
}
}

3
application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java

@ -73,6 +73,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo());
edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME);
@ -229,6 +230,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
config.setOutput(output);
calculatedField.setConfiguration(config);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField;
}
@ -260,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName());
Assert.assertEquals(JacksonUtil.newObjectNode(), calculatedField.getAdditionalInfo());
}
}

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -30,6 +30,7 @@ import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -375,6 +376,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(aiModelUpdateMsg));
}
}
if (downlinkMsg.getApiKeyUpdateMsgCount() > 0) {
for (ApiKeyUpdateMsg apiKeyUpdateMsg : downlinkMsg.getApiKeyUpdateMsgList()) {
result.add(saveDownlinkMsg(apiKeyUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
}

272
application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java

@ -0,0 +1,272 @@
/**
* 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.service.edge.rpc;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
/**
* Tests for Edge gRPC SSL setup using the production {@link EdgeGrpcService#setupSsl} method.
* <p>
* Covers:
* 1. Separate cert and key PEM inputs
* 2. Combined PEM (cert + key in one file)
* 3. Encrypted private key with password
* 4. Missing key in combined PEM → error
* <p>
* Each scenario is parameterized across key types: RSA-2048, RSA-4096, EC P-256, EC P-384.
*/
class EdgeGrpcSslTest {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
enum KeyType {
RSA_2048("RSA", 2048, null, "SHA256withRSA"),
RSA_4096("RSA", 4096, null, "SHA256withRSA"),
EC_P256("EC", 256, "secp256r1", "SHA256withECDSA"),
EC_P384("EC", 384, "secp384r1", "SHA384withECDSA");
final String algorithm;
final int size;
final String curve;
final String sigAlg;
KeyType(String algorithm, int size, String curve, String sigAlg) {
this.algorithm = algorithm;
this.size = size;
this.curve = curve;
this.sigAlg = sigAlg;
}
KeyPair generateKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm);
if (curve != null) {
kpg.initialize(new ECGenParameterSpec(curve));
} else {
kpg.initialize(size);
}
return kpg.generateKeyPair();
}
}
private final List<Path> tempFiles = new ArrayList<>();
private Server server;
private ManagedChannel channel;
@AfterEach
void cleanup() throws Exception {
if (channel != null) {
channel.shutdownNow().awaitTermination(2, TimeUnit.SECONDS);
}
if (server != null) {
server.shutdownNow().awaitTermination(2, TimeUnit.SECONDS);
}
for (Path p : tempFiles) {
Files.deleteIfExists(p);
}
}
@ParameterizedTest(name = "separateCertAndKey_{0}")
@EnumSource(KeyType.class)
void separateCertAndKey(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
Path certFile = writeTempPem("cert", cert);
Path keyFile = writeTempPem("key", kp.getPrivate());
server = startServer(certFile.toString(), keyFile.toString(), null);
assertTlsConnectivity(cert);
}
@ParameterizedTest(name = "combinedPemWithCertAndKey_{0}")
@EnumSource(KeyType.class)
void combinedPemWithCertAndKey(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
Path combinedFile = writeTempPem("combined", cert, kp.getPrivate());
server = startServer(combinedFile.toString(), "", null);
assertTlsConnectivity(cert);
}
// RSA-only: BouncyCastle writes encrypted EC keys in traditional PEM format (BEGIN EC PRIVATE KEY),
// which after decryption produces a PEMKeyPair without public key info — causing PemSslCredentials
// to fail with "Cannot invoke SubjectPublicKeyInfo.getEncoded() because getPublicKeyInfo() is null".
@ParameterizedTest(name = "encryptedPrivateKey_{0}")
@EnumSource(value = KeyType.class, names = {"RSA_2048", "RSA_4096"})
void encryptedPrivateKey(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
String password = "test-password";
Path combinedFile = writeTempPemEncrypted("enc-combined", password, cert, kp.getPrivate());
server = startServer(combinedFile.toString(), "", password);
assertTlsConnectivity(cert);
}
@ParameterizedTest(name = "combinedPemWithCertOnly_throwsException_{0}")
@EnumSource(KeyType.class)
void combinedPemWithCertOnly_throwsException(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
Path certOnlyFile = writeTempPem("cert-only", cert);
assertThatThrownBy(() -> startServer(certOnlyFile.toString(), "", null))
.isInstanceOf(IllegalArgumentException.class);
}
// --- Server startup using production EdgeGrpcService.setupSsl() ---
private Server startServer(String certFileResource, String privateKeyResource, String keyPassword) throws Exception {
EdgeGrpcService edgeGrpcService = new EdgeGrpcService();
ReflectionTestUtils.setField(edgeGrpcService, "certFileResource", certFileResource);
ReflectionTestUtils.setField(edgeGrpcService, "privateKeyResource", privateKeyResource);
ReflectionTestUtils.setField(edgeGrpcService, "keyPassword", keyPassword != null ? keyPassword : "");
NettyServerBuilder builder = NettyServerBuilder.forPort(0)
.addService(new EdgeRpcServiceGrpc.EdgeRpcServiceImplBase() {});
edgeGrpcService.setupSsl(builder);
return builder.build().start();
}
private void assertTlsConnectivity(X509Certificate trustedCert) throws Exception {
String certPem = toPem(trustedCert);
var clientSsl = GrpcSslContexts.forClient()
.trustManager(new ByteArrayInputStream(certPem.getBytes(StandardCharsets.UTF_8)))
.build();
channel = NettyChannelBuilder.forAddress("localhost", server.getPort())
.sslContext(clientSsl)
.build();
channel.getState(true); // trigger connection attempt
await().atMost(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS)
.pollInterval(50, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
var state = channel.getState(false);
if (state == io.grpc.ConnectivityState.TRANSIENT_FAILURE) {
throw new AssertionError("TLS handshake failed: channel in TRANSIENT_FAILURE");
}
assertThat(state).isEqualTo(io.grpc.ConnectivityState.READY);
});
}
// --- Cert/key generation ---
private X509Certificate generateSelfSignedCert(KeyPair kp, String sigAlg) throws Exception {
X500Name subject = new X500Name("CN=localhost");
Date now = new Date();
return new JcaX509CertificateConverter().getCertificate(
new JcaX509v3CertificateBuilder(
subject, BigInteger.ONE, now,
new Date(now.getTime() + TimeUnit.DAYS.toMillis(1)),
subject, kp.getPublic())
.build(new JcaContentSignerBuilder(sigAlg).build(kp.getPrivate())));
}
// --- PEM file helpers ---
private String toPem(Object obj) throws Exception {
java.io.StringWriter sw = new java.io.StringWriter();
try (JcaPEMWriter w = new JcaPEMWriter(sw)) {
w.writeObject(obj);
}
return sw.toString();
}
private Path writeTempPem(String prefix, Object... objects) throws Exception {
Path p = Files.createTempFile(prefix + "-", ".pem");
tempFiles.add(p);
try (JcaPEMWriter w = new JcaPEMWriter(Files.newBufferedWriter(p))) {
for (Object o : objects) {
w.writeObject(toPkcs8IfKey(o));
}
}
return p;
}
private Path writeTempPemEncrypted(String prefix, String password, Object... objects) throws Exception {
Path p = Files.createTempFile(prefix + "-", ".pem");
tempFiles.add(p);
var encryptor = new JcePEMEncryptorBuilder("AES-256-CBC")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(password.toCharArray());
try (JcaPEMWriter w = new JcaPEMWriter(Files.newBufferedWriter(p))) {
for (Object o : objects) {
if (o instanceof PrivateKey) {
w.writeObject(o, encryptor);
} else {
w.writeObject(o);
}
}
}
return p;
}
private Object toPkcs8IfKey(Object o) {
if (o instanceof PrivateKey pk) {
return new PemObject("PRIVATE KEY", pk.getEncoded());
}
return o;
}
}

8
application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

@ -115,6 +115,8 @@ import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -1749,13 +1751,13 @@ public class EntityServiceTest extends AbstractControllerTest {
}
@Test
public void testFindTenantTelemetry() {
public void testFindTenantTelemetry() throws ExecutionException, InterruptedException, TimeoutException {
// save timeseries by sys admin
BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, new DoubleDataEntry("temperature", 45.5));
timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries);
timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries).get(TIMEOUT, TimeUnit.SECONDS);
AttributeKvEntry attr = new BaseAttributeKvEntry(new LongDataEntry("attr", 10L), 42L);
attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr));
attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr)).get(TIMEOUT, TimeUnit.SECONDS);
SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId));

10
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -23,8 +23,8 @@ import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
@ -127,10 +127,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
public class HousekeeperServiceTest extends AbstractControllerTest {
@SpyBean
@MockitoSpyBean
private HousekeeperService housekeeperService;
@SpyBean
@MockitoSpyBean
private HousekeeperReprocessingService housekeeperReprocessingService;
@MockitoSpyBean
private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
@Autowired
private EventService eventService;
@Autowired
@ -153,8 +155,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
private CustomerService customerService;
@Autowired
private DashboardService dashboardService;
@SpyBean
private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
private TenantId tenantId;

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

144
application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java

@ -0,0 +1,144 @@
/**
* 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.service.ttl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.notification.NotificationRequestDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class NotificationsCleanUpServiceTest {
@Mock
private PartitionService partitionService;
@Mock
private SqlPartitioningRepository partitioningRepository;
@Mock
private NotificationRequestDao notificationRequestDao;
@Mock
private TenantService tenantService;
private NotificationsCleanUpService cleanUpService;
private static final int BATCH_SIZE = 3;
@BeforeEach
public void setUp() {
cleanUpService = new NotificationsCleanUpService(partitionService, partitioningRepository, notificationRequestDao, tenantService);
ReflectionTestUtils.setField(cleanUpService, "ttlInSec", 2592000L);
ReflectionTestUtils.setField(cleanUpService, "partitionSizeInHours", 168);
ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE);
}
@Test
public void testBatchLoopCallsDaoMultipleTimes() {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition);
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
.thenReturn(System.currentTimeMillis());
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(tenantId), 1, 1, false));
// Sysadmin: returns 3 (full batch), then 1 (partial) -> 2 calls
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
.thenReturn(BATCH_SIZE)
.thenReturn(1);
// Tenant: returns 3, 3, 0 -> 3 calls
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)))
.thenReturn(BATCH_SIZE)
.thenReturn(BATCH_SIZE)
.thenReturn(0);
cleanUpService.cleanUp();
verify(notificationRequestDao, times(2))
.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE));
verify(notificationRequestDao, times(3))
.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE));
}
@Test
public void testSkipsTenantNotOnMyPartition() {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build();
when(partitionService.resolve(any(), eq(TenantId.SYS_TENANT_ID), eq(TenantId.SYS_TENANT_ID)))
.thenReturn(myPartition);
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
.thenReturn(System.currentTimeMillis());
// Sysadmin: no records
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
TenantId myTenant = TenantId.fromUUID(UUID.randomUUID());
TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID());
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false));
when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition);
when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition);
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
cleanUpService.cleanUp();
verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE));
verify(notificationRequestDao, never()).removeByTenantIdAndCreatedTimeBeforeBatch(eq(otherTenant), anyLong(), anyInt());
}
@Test
public void testNoPartitionsDropped_stillCleansUpRequests() {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition);
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
.thenReturn(0L);
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(), 0, 0, false));
cleanUpService.cleanUp();
verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE));
}
}

136
application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java

@ -0,0 +1,136 @@
/**
* 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.service.ttl.rpc;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.rpc.RpcDao;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class RpcCleanUpServiceTest {
@Mock
private PartitionService partitionService;
@Mock
private RpcDao rpcDao;
@Mock
private TenantService tenantService;
@Mock
private TbTenantProfileCache tenantProfileCache;
private RpcCleanUpService cleanUpService;
private static final int BATCH_SIZE = 3;
@BeforeEach
public void setUp() {
cleanUpService = new RpcCleanUpService(tenantService, partitionService, tenantProfileCache, rpcDao);
ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE);
}
@Test
public void testBatchLoopCallsDaoMultipleTimes() {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
setupTenant(tenantId, 7);
// Returns 3 (full batch), 3 (full batch), 1 (partial) -> 3 calls
when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)))
.thenReturn(BATCH_SIZE)
.thenReturn(BATCH_SIZE)
.thenReturn(1);
cleanUpService.cleanUp();
verify(rpcDao, times(3)).deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE));
}
@Test
public void testSkipsTenantNotOnMyPartition() {
TenantId myTenant = TenantId.fromUUID(UUID.randomUUID());
TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID());
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build();
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false));
when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition);
when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition);
setupTenantProfile(myTenant, 7);
when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
cleanUpService.cleanUp();
verify(rpcDao).deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE));
verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(eq(otherTenant), anyLong(), anyInt());
}
@Test
public void testSkipsTenantWithZeroTtl() {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
setupTenant(tenantId, 0);
cleanUpService.cleanUp();
verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(any(), anyLong(), anyInt());
}
private void setupTenant(TenantId tenantId, int rpcTtlDays) {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(myPartition);
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(tenantId), 1, 1, false));
setupTenantProfile(tenantId, rpcTtlDays);
}
private void setupTenantProfile(TenantId tenantId, int rpcTtlDays) {
TenantProfile profile = new TenantProfile();
TenantProfileData profileData = new TenantProfileData();
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
config.setRpcTtlDays(rpcTtlDays);
profileData.setConfiguration(config);
profile.setProfileData(profileData);
when(tenantProfileCache.get(tenantId)).thenReturn(profile);
}
}

9
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java

@ -171,7 +171,14 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
if (this.leshanClient != null) {
log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.stop(false);
try {
this.leshanClient.stop(false);
} catch (Exception stopEx) {
// Leshan may throw NPE during CoAP observe-relation cleanup when the server
// reference is null (race condition in NotificationDataStore.toKey()).
// The client is still considered stopped at this point — proceed with restart.
log.warn("Exception during LwM2M client stop, proceeding with restart: {}", stopEx.getMessage());
}
log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.start();

7
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -37,6 +38,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
@Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest {
@Before
public void resetCollectedValueTimestamps() {
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = 0;
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = 0;
}
/**
* Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload

8
common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java

@ -24,10 +24,14 @@ import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
public interface ApiKeyService extends EntityDaoService {
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey);
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo, String value, boolean doValidate);
void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force);
void deleteByUserId(TenantId tenantId, UserId userId);
@ -38,4 +42,8 @@ public interface ApiKeyService extends EntityDaoService {
PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink);
List<ApiKey> findApiKeysByUserId(TenantId tenantId, UserId userId);
PageData<ApiKey> findApiKeysByTenantId(TenantId tenantId, PageLink pageLink);
}

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;
@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> {
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;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@JsonPropertyOrder({
"emailEnabled",
"smsEnabled",
"notificationEnabled",
"oauthEnabled",
"twoFaEnabled"
})
@Schema
@Data
public class FeaturesInfo {
@JsonProperty("emailEnabled")
boolean isEmailEnabled;
@JsonProperty("smsEnabled")
boolean isSmsEnabled;
@JsonProperty("notificationEnabled")
boolean isNotificationEnabled;
@JsonProperty("oauthEnabled")
boolean isOauthEnabled;
@JsonProperty("twoFaEnabled")
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;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -39,9 +40,15 @@ public class ShortCustomerInfo {
@NoXss
private String title;
@JsonProperty("isPublic")
@Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.")
@Getter @Setter
private boolean isPublic;
private boolean publicCustomer;
@JsonProperty("isPublic")
public boolean isPublic() { return publicCustomer; }
@JsonProperty("isPublic")
public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; }
@Override
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;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -23,6 +24,7 @@ import java.util.List;
@Data
public class SystemInfo {
@Schema(description = "Is monolith.")
@JsonProperty("monolith")
private boolean isMonolith;
@Schema(description = "System data.")
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")
private long assignTs;
@Schema(description = "JSON object with alarm details")
@JsonProperty
private transient JsonNode details;
@Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true")
private boolean propagate;

31
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -17,15 +17,15 @@ package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
@ -48,10 +47,7 @@ import java.util.Set;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings {
@Serial
private static final long serialVersionUID = 4491966747773381420L;
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo {
public static final Map<EntityType, Set<CalculatedFieldType>> SUPPORTED_ENTITIES = Map.of(
EntityType.DEVICE, CalculatedFieldType.all,
@ -90,28 +86,17 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Valid
@NotNull
private CalculatedFieldConfiguration configuration;
@Getter
@Setter
private Long version;
@NoXss
@Schema(description = "Additional parameters of the calculated field")
private JsonNode additionalInfo;
public CalculatedField() {
super();
}
public CalculatedField() {}
public CalculatedField(CalculatedFieldId id) {
super(id);
}
public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) {
this.tenantId = tenantId;
this.entityId = entityId;
this.type = type;
this.name = name;
this.configurationVersion = configurationVersion;
this.configuration = configuration;
this.version = version;
}
public CalculatedField(CalculatedField calculatedField) {
super(calculatedField);
this.tenantId = calculatedField.tenantId;
@ -123,6 +108,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration;
this.version = calculatedField.version;
this.additionalInfo = calculatedField.additionalInfo;
}
@Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.")
@ -159,6 +145,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
.append(", name='").append(name)
.append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration)
.append(", additionalInfo=").append(additionalInfo)
.append(", version=").append(version)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')

3
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java

@ -48,7 +48,8 @@ public enum EdgeEventType {
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD),
AI_MODEL(true, EntityType.AI_MODEL);
AI_MODEL(true, EntityType.AI_MODEL),
API_KEY(true, EntityType.API_KEY);
private final boolean allEdgesRelated;

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;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
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")
protected String server;
@Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"})
@JsonProperty("isError")
protected boolean isError;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr;
@JsonProperty("isError")
public boolean isError() {
return isError;
}
@JsonProperty("isError")
public void setIsError(boolean isError) {
this.isError = isError;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java

@ -23,6 +23,7 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.util.List;
import java.util.UUID;
@ -32,6 +33,9 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AlarmsDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 9214680001573764374L;
private List<UUID> alarms;
public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) {

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import java.io.Serial;
import java.util.List;
import java.util.UUID;
@ -33,6 +34,9 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AlarmsUnassignHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 9156667024462937756L;
private String userTitle;
private List<UUID> alarms;

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java

@ -23,6 +23,7 @@ import lombok.ToString;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.util.List;
import java.util.UUID;
@ -32,6 +33,9 @@ import java.util.UUID;
@NoArgsConstructor
public class EntitiesDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 9009068831061529286L;
private EntityType entityType;
private List<UUID> entities;

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true)
@ -45,6 +46,9 @@ import java.io.Serializable;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class HousekeeperTask implements Serializable {
@Serial
private static final long serialVersionUID = -2585974110832225152L;
private TenantId tenantId;
private EntityId entityId;
private HousekeeperTaskType taskType;

5
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java

@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LatestTsDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 5193191938513490138L;
private String key;
public LatestTsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) {

5
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java

@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class TenantEntitiesDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = -8033108795318393447L;
private EntityType entityType;
public TenantEntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) {

5
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java

@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 4573851542705079043L;
private String key;
public TsHistoryDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) {

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.util.UUID;
@Schema(allOf = EntityId.class)
public class AdminSettingsId extends UUIDBased implements EntityId {
@Serial
@ -33,7 +34,7 @@ public class AdminSettingsId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.util.UUID;
@Schema(allOf = EntityId.class)
public final class AiModelId extends UUIDBased implements EntityId {
@Serial
@ -36,6 +37,7 @@ public final class AiModelId extends UUIDBased implements EntityId {
@Override
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY,
description = "Entity type of the AI model",
example = "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;
@Schema
@Schema(allOf = EntityId.class)
public class AlarmId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.util.UUID;
@Schema(allOf = EntityId.class)
public class ApiKeyId extends UUIDBased implements EntityId {
@Serial
@ -38,7 +39,7 @@ public class ApiKeyId extends UUIDBased implements EntityId {
}
@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() {
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;
@Schema
@Schema(allOf = EntityId.class)
public class ApiUsageStateId extends UUIDBased implements EntityId {
@JsonCreator
@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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;
@Schema
@Schema(allOf = EntityId.class)
public class AssetId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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;
@Schema(allOf = EntityId.class)
public class AssetProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class AssetProfileId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.util.UUID;
@Schema
@Schema(allOf = EntityId.class)
public class CalculatedFieldId extends UUIDBased implements EntityId {
@Serial
@ -38,7 +38,7 @@ public class CalculatedFieldId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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;
@Schema
@Schema(allOf = EntityId.class)
public final class CustomerId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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;
@Schema
@Schema(allOf = EntityId.class)
public class DashboardId extends UUIDBased implements EntityId {
@JsonCreator
@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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;
@Schema
@Schema(allOf = EntityId.class)
public class DeviceId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId {
}
@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() {
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;
@Schema(allOf = EntityId.class)
public class DeviceProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Schema(allOf = EntityId.class)
public class DomainId extends UUIDBased implements EntityId {
@JsonCreator
@ -32,6 +34,7 @@ public class DomainId extends UUIDBased implements EntityId {
return new DomainId(UUID.fromString(oauth2DomainId));
}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DOMAIN", allowableValues = "DOMAIN")
@Override
public EntityType getEntityType() {
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.util.UUID;
@Schema(allOf = EntityId.class)
public class EdgeId extends UUIDBased implements EntityId {
@Serial
@ -43,7 +44,7 @@ public class EdgeId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
@ -26,7 +27,47 @@ import java.util.UUID;
@JsonDeserialize(using = EntityIdDeserializer.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
UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080");

1
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -114,6 +114,7 @@ public class EntityIdFactory {
case DOMAIN -> new DomainId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case AI_MODEL -> new AiModelId(uuid);
case API_KEY -> new ApiKeyId(uuid);
case ADMIN_SETTINGS -> new AdminSettingsId(uuid);
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
};

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.
*/
@Schema(allOf = EntityId.class)
public class EntityViewId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@ -38,7 +39,7 @@ public class EntityViewId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.util.UUID;
@Schema(allOf = EntityId.class)
public class JobId extends UUIDBased implements EntityId {
@Serial
@ -33,7 +34,7 @@ public class JobId extends UUIDBased implements EntityId {
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
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Schema(allOf = EntityId.class)
public class MobileAppBundleId extends UUIDBased implements EntityId{
@JsonCreator
@ -32,6 +34,7 @@ public class MobileAppBundleId extends UUIDBased implements EntityId{
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
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Schema(allOf = EntityId.class)
public class MobileAppId extends UUIDBased implements EntityId{
@JsonCreator
@ -32,6 +34,7 @@ public class MobileAppId extends UUIDBased implements EntityId{
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
public EntityType getEntityType() {
return EntityType.MOBILE_APP;

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

Loading…
Cancel
Save