Browse Source

Merge branch 'master' into fix/rest-api-node/url-encoding

# Conflicts:
#	rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
pull/14540/head
Dmytro Skarzhynets 7 months ago
parent
commit
042937738b
  1. 4
      .github/release.yml
  2. 2
      .github/workflows/license-header-format.yml
  3. 1
      .gitignore
  4. 39
      TEST_FAST.md
  5. 76
      application/pom.xml
  6. 36
      application/src/main/data/json/edge/instructions/install/centos/instructions.md
  7. 12
      application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md
  8. 2
      application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
  9. 2
      application/src/main/data/json/system/widget_types/range_chart.json
  10. 5
      application/src/main/data/upgrade/basic/schema_update.sql
  11. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  12. 15
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  13. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  14. 341
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  15. 13
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  16. 4
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  17. 4
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  18. 42
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  19. 24
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  20. 15
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  21. 4
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  22. 44
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  23. 4
      application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
  24. 24
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  25. 37
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  26. 28
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  27. 43
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  28. 30
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  29. 6
      application/src/main/java/org/thingsboard/server/controller/DomainController.java
  30. 42
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  31. 9
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  32. 58
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  33. 168
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  34. 46
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  35. 19
      application/src/main/java/org/thingsboard/server/controller/EventController.java
  36. 4
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  37. 4
      application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java
  38. 6
      application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java
  39. 4
      application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
  40. 41
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  41. 4
      application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java
  42. 15
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  43. 30
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  44. 2
      application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java
  45. 8
      application/src/main/java/org/thingsboard/server/controller/QueueController.java
  46. 17
      application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java
  47. 8
      application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java
  48. 8
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  49. 32
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  50. 26
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  51. 19
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  52. 95
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  53. 18
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  54. 15
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  55. 9
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  56. 4
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  57. 71
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  58. 30
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  59. 31
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  60. 28
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  61. 12
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  62. 38
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  63. 43
      application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java
  64. 77
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  65. 36
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  66. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  67. 34
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
  68. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
  69. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  70. 105
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java
  71. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java
  72. 63
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java
  73. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java
  74. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  75. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  76. 3
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  77. 92
      application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java
  78. 145
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  79. 7
      application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
  80. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java
  81. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java
  82. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java
  83. 23
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  84. 4
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  85. 4
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
  86. 75
      application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java
  87. 94
      application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java
  88. 3
      application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java
  89. 51
      application/src/main/resources/thingsboard-openapi.properties
  90. 31
      application/src/main/resources/thingsboard.yml
  91. 43
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  92. 11
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  93. 68
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  94. 71
      application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java
  95. 39
      application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
  96. 4
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  97. 7
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  98. 6
      application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
  99. 237
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  100. 7
      application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java

4
.github/release.yml

@ -19,6 +19,10 @@ changelog:
labels: labels:
- Ignore for release - Ignore for release
categories: categories:
- title: 'Security'
labels:
- 'Security'
- title: 'Major Core & Rule Engine' - title: 'Major Core & Rule Engine'
labels: labels:
- 'Major Core' - 'Major Core'

2
.github/workflows/license-header-format.yml

@ -35,7 +35,7 @@ jobs:
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'corretto' # https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions distribution: 'corretto' # https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions
java-version: '21' java-version: '25'
cache: 'maven' # https://github.com/actions/setup-java?tab=readme-ov-file#caching-sbt-dependencies cache: 'maven' # https://github.com/actions/setup-java?tab=readme-ov-file#caching-sbt-dependencies
- name: License header format - name: License header format

1
.gitignore

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

39
TEST_FAST.md

@ -10,22 +10,43 @@ mvn clean install -T6 -DskipTests
mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4 mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4
mvn test -pl dao -Dparallel=packages -DforkCount=4 mvn test -pl dao -Dparallel=packages -DforkCount=4
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 mvn test -pl application -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
#the rest of application tests #the rest of application tests
mvn test -pl application -Dtest=' mvn test -pl application -Dtest='
!**/nosql/*Test.java, !**/nosql/**,
!org.thingsboard.server.controller.**, !org.thingsboard.server.controller.**,
!org.thingsboard.server.edge.**, !org.thingsboard.server.edge.**,
!org.thingsboard.server.service.**, !org.thingsboard.server.service.**,
!org.thingsboard.server.transport.mqtt.**, !org.thingsboard.server.transport.mqtt.**,
!org.thingsboard.server.transport.coap.**, !org.thingsboard.server.transport.coap.**,
!org.thingsboard.server.transport.lwm2m.** !org.thingsboard.server.transport.lwm2m.**,
!**/*TestSuite.java
' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 ' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
``` ```
## Testcontainers compatibility with the Docker API workaround
In case your tests failed to run testcontainers due to unsupported Docker API version
:coffee: testcontainers (Docker API 1.32) + :whale: docker 29 (min API 1.44) workaround
Add to /etc/docker/daemon.json and restart docker
```json
{
"min-api-version": "1.32"
}
```
Same works on Mac, except `daemon.json` are located in another folder and required to be edited from Docker Desktop UI.
Tip: If your testcontainer are struggling to find any Docker. You can try to remove the testcontainers property file. It will be recreated on the next testcontainers run.
```bash
rm ~/.testcontainers.properties
```

76
application/pom.xml

@ -507,6 +507,82 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<profiles>
<profile>
<id>openapi-spec</id>
<properties>
<pkg.disabled>true</pkg.disabled>
<pkg.package.phase>none</pkg.package.phase>
<pkg.process-resources.phase>none</pkg.process-resources.phase>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>org.thingsboard.server.ThingsboardServerApplication</mainClass>
<jmxPort>9001</jmxPort>
</configuration>
<executions>
<execution>
<id>build-info</id>
<goals><goal>build-info</goal></goals>
<configuration>
<skip>false</skip>
</configuration>
</execution>
<execution>
<id>openapi-start</id>
<goals><goal>start</goal></goals>
<configuration>
<skip>false</skip>
<useTestClasspath>true</useTestClasspath>
<jvmArguments>-Xmx1024m</jvmArguments>
<arguments>
<argument>--spring.config.name=thingsboard</argument>
<argument>--spring.profiles.active=openapi</argument>
</arguments>
<maxAttempts>180</maxAttempts>
<wait>2000</wait>
</configuration>
</execution>
<execution>
<id>openapi-stop</id>
<goals><goal>stop</goal></goals>
<configuration>
<skip>false</skip>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>generate-openapi-spec</id>
<goals><goal>generate</goal></goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs/thingsboard</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories> <repositories>
<repository> <repository>
<id>jenkins</id> <id>jenkins</id>

36
application/src/main/data/json/edge/instructions/install/centos/instructions.md

@ -1,22 +1,24 @@
Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 9/10 and connect to the server.
**Note:** OpenJDK 25 requires RHEL/CentOS 9+ or RHEL/CentOS 10+. Earlier versions (RHEL/CentOS 7, 8) are not supported.
#### Prerequisites #### Prerequisites
Before continuing to installation, execute the following commands to install the necessary tools: Before continuing to installation, execute the following commands to install the necessary tools:
```bash ```bash
sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm sudo dnf install -y nano wget
{:copy-code} {:copy-code}
``` ```
#### Step 1. Install Java 17 (OpenJDK) #### Step 1. Install Java 25 (OpenJDK)
ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: ThingsBoard service is running on Java 25. To install OpenJDK 25, follow these instructions:
```bash ```bash
sudo dnf install java-17-openjdk sudo dnf install -y java-25-openjdk
{:copy-code} {:copy-code}
``` ```
Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: Configure your operating system to use OpenJDK 25 by default. You can configure the default version by running the following command:
```bash ```bash
sudo update-alternatives --config java sudo update-alternatives --config java
@ -33,7 +35,7 @@ java -version
The expected result is: The expected result is:
```text ```text
openjdk version "17.x.xx" openjdk version "25.x.xx"
OpenJDK Runtime Environment (...) OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...) OpenJDK 64-Bit Server VM (build ...)
``` ```
@ -41,7 +43,7 @@ OpenJDK 64-Bit Server VM (build ...)
#### Step 2. Configure ThingsBoard Edge Database #### Step 2. Configure ThingsBoard Edge Database
ThingsBoard Edge supports **SQL** and **hybrid** database configurations. ThingsBoard Edge supports **SQL** and **hybrid** database configurations.
In this guide, we’ll use an **SQL** database. In this guide, we'll use an **SQL** database.
For more details about the hybrid setup, please refer to the official installation instructions on the <a href="https://thingsboard.io/docs/user-guide/install/edge/rhel/#step-2-configure-thingsboard-database" target="_blank">ThingsBoard documentation site</a>. For more details about the hybrid setup, please refer to the official installation instructions on the <a href="https://thingsboard.io/docs/user-guide/install/edge/rhel/#step-2-configure-thingsboard-database" target="_blank">ThingsBoard documentation site</a>.
To install the PostgreSQL database, run these commands: To install the PostgreSQL database, run these commands:
@ -54,19 +56,19 @@ sudo dnf update
Install the repository RPM: Install the repository RPM:
* **For CentOS/RHEL 8:** * **For RHEL/CentOS 9:**
```bash ```bash
# Install the repository RPM (For CentOS/RHEL 8): # Install the repository RPM (for RHEL/CentOS 9):
sudo sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
{:copy-code} {:copy-code}
``` ```
* **For CentOS/RHEL 9:** * **For RHEL/CentOS 10:**
```bash ```bash
# Install the repository RPM (for CentOS 9): # Install the repository RPM (for RHEL/CentOS 10):
sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-x86_64/pgdg-redhat-repo-latest.noarch.rpm
{:copy-code} {:copy-code}
``` ```
@ -91,8 +93,8 @@ sudo -u postgres psql -c "\password"
Then, enter and confirm the password. Then, enter and confirm the password.
Since ThingsBoard Edge uses the PostgreSQL database for local storage, configuring MD5 authentication ensures that only authenticated users or Since ThingsBoard Edge uses the PostgreSQL database for local storage, configuring MD5 authentication ensures that only authenticated users or
applications can access the database, thus protecting your data. After configuring the password, applications can access the database, thus protecting your data. After configuring the password,
edit the pg_hba.conf file to use MD5 hashing for authentication instead of the default method (ident) for local IPv4 connections. edit the pg_hba.conf file to use MD5 hashing for authentication instead of the default method (ident) for local IPv4 connections.
To replace ident with md5, run the following command: To replace ident with md5, run the following command:
@ -102,7 +104,7 @@ sudo sed -i 's/^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+ident/host all
{:copy-code} {:copy-code}
``` ```
Then run the command that will restart the PostgreSQL service to apply configuration changes, connect to the database as a postgres user, Then run the command that will restart the PostgreSQL service to apply configuration changes, connect to the database as a postgres user,
and create the ThingsBoard Edge database (tb_edge). To connect to the PostgreSQL database, enter the PostgreSQL password. and create the ThingsBoard Edge database (tb_edge). To connect to the PostgreSQL database, enter the PostgreSQL password.
```bash ```bash

12
application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md

@ -1,14 +1,16 @@
Here is the list of commands that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. Here is the list of commands that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server.
#### Step 1. Install Java 17 (OpenJDK) **Note:** OpenJDK 25 requires Ubuntu 22.04 LTS or newer. Earlier versions (20.04 and below) are not supported.
ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions:
#### Step 1. Install Java 25 (OpenJDK)
ThingsBoard service is running on Java 25. To install OpenJDK 25, follow these instructions:
```bash ```bash
sudo apt update && sudo apt install openjdk-17-jdk sudo apt update && sudo apt install openjdk-25-jdk
{:copy-code} {:copy-code}
``` ```
Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: Configure your operating system to use OpenJDK 25 by default. You can configure the default version by running the following command:
```bash ```bash
sudo update-alternatives --config java sudo update-alternatives --config java
@ -25,7 +27,7 @@ java -version
The expected result is: The expected result is:
```text ```text
openjdk version "17.x.xx" openjdk version "25.x.xx"
OpenJDK Runtime Environment (...) OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...) OpenJDK 64-Bit Server VM (build ...)
``` ```

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

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<tb-bar-chart-with-labels-widget \n [ctx]=\"ctx\">\n</tb-bar-chart-with-labels-widget>", "templateHtml": "<tb-bar-chart-with-labels-widget \n [ctx]=\"ctx\">\n</tb-bar-chart-with-labels-widget>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n",
"settingsForm": [], "settingsForm": [],
"dataKeySettingsForm": [], "dataKeySettingsForm": [],
"latestDataKeySettingsForm": [], "latestDataKeySettingsForm": [],

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

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<tb-range-chart-widget \n [ctx]=\"ctx\">\n</tb-range-chart-widget>", "templateHtml": "<tb-range-chart-widget \n [ctx]=\"ctx\">\n</tb-range-chart-widget>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n",
"settingsForm": [], "settingsForm": [],
"dataKeySettingsForm": [], "dataKeySettingsForm": [],
"latestDataKeySettingsForm": [], "latestDataKeySettingsForm": [],

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

@ -14,3 +14,8 @@
-- limitations under the License. -- 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

@ -31,6 +31,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
@ -144,6 +145,7 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -614,9 +616,17 @@ public class ActorSystemContext {
@Getter @Getter
private boolean localCacheType; private boolean localCacheType;
@Value("${actors.rule.external.ssrf_protection_enabled:false}")
private boolean ssrfProtectionEnabled;
@Value("${actors.rule.external.ssrf_additional_blocked_hosts:}")
private List<String> ssrfAdditionalBlockedHosts;
@PostConstruct @PostConstruct
public void init() { public void init() {
this.localCacheType = "caffeine".equals(cacheType); this.localCacheType = "caffeine".equals(cacheType);
SsrfProtectionValidator.setEnabled(ssrfProtectionEnabled);
SsrfProtectionValidator.setAdditionalBlockedHosts(ssrfAdditionalBlockedHosts);
} }
@Value("${actors.tenant.create_components_on_init:true}") @Value("${actors.tenant.create_components_on_init:true}")

15
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -184,11 +184,24 @@ public class DefaultTbContext implements TbContext {
if (!msg.isValid()) { if (!msg.isValid()) {
return; return;
} }
RuleChainId selfRuleChainId = nodeCtx.getSelf().getRuleChainId();
RuleNodeId selfId = nodeCtx.getSelf().getId();
if (msg.isAlreadyInStack(selfRuleChainId, selfId)) {
log.warn("[{}] Detected rule chain processing loop for rule node [{}] in rule chain [{}]. " +
"The message will be failed to prevent infinite loop. " +
"Please check the rule chain configuration for circular references.",
nodeCtx.getTenantId(), selfId, selfRuleChainId);
tellFailure(msg, new RuntimeException(
"Detected rule chain processing loop for rule node [" + selfId + "] " +
"in rule chain [" + selfRuleChainId + "]. " +
"Please check the rule chain configuration for circular references."));
return;
}
TbMsg tbMsg = msg.copy() TbMsg tbMsg = msg.copy()
.ruleChainId(ruleChainId) .ruleChainId(ruleChainId)
.resetRuleNodeId() .resetRuleNodeId()
.build(); .build();
tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); tbMsg.pushToStack(selfRuleChainId, selfId);
TopicPartitionInfo tpi = resolvePartition(msg); TopicPartitionInfo tpi = resolvePartition(msg);
doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t))); doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t)));
} }

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -137,7 +137,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
defaultCtx.tellFailure(msg.getMsg(), e); defaultCtx.tellFailure(msg.getMsg(), e);
} }
} else { } else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode)); tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more than " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
} }
} }
@ -160,7 +160,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
msg.getCtx().tellFailure(msg.getMsg(), e); msg.getCtx().tellFailure(msg.getMsg(), e);
} }
} else { } else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode)); tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more than " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
} }
} }
} }

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

@ -17,9 +17,11 @@ package org.thingsboard.server.config;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
@ -31,6 +33,7 @@ import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.parameters.RequestBody;
@ -39,10 +42,10 @@ import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.tags.Tag; import io.swagger.v3.oas.models.tags.Tag;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer; import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.RouterOperationCustomizer;
import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer;
import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SpringDocConfigProperties;
@ -54,26 +57,27 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestParam;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse; import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -83,11 +87,16 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Profile("!test") @Profile("!test")
public class SwaggerConfiguration { public class SwaggerConfiguration {
@PostConstruct
public void configureModelResolver() {
ModelResolver.enumsAsRef = true;
}
public static final String LOGIN_ENDPOINT = "/api/auth/login"; public static final String LOGIN_ENDPOINT = "/api/auth/login";
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token"; public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token";
private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form"; private static final String LOGIN_PASSWORD_SCHEME = "HttpLoginForm";
private static final String API_KEY_SCHEME = "API key form"; private static final String API_KEY_SCHEME = "ApiKeyForm";
private static final ApiResponses loginResponses = loginResponses(); private static final ApiResponses loginResponses = loginResponses();
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
@ -157,9 +166,9 @@ public class SwaggerConfiguration {
.in(SecurityScheme.In.HEADER) .in(SecurityScheme.In.HEADER)
.description(""" .description("""
Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>** Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>**
Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW** Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW**
<br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br> <br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br>
"""); """);
@ -213,11 +222,12 @@ public class SwaggerConfiguration {
private void addLoginOperation(OpenAPI openAPI) { private void addLoginOperation(OpenAPI openAPI) {
var operation = new Operation(); var operation = new Operation();
operation.summary("Login method to get user JWT token data"); operation.summary("Login method to get user JWT token data");
operation.operationId("login");
operation.description(""" operation.description("""
Login method used to authenticate user and get JWT token data. Login method used to authenticate user and get JWT token data.
Value of the response **token** field can be used as **X-Authorization** header value: Value of the response **token** field can be used as **X-Authorization** header value:
`X-Authorization: Bearer $JWT_TOKEN_VALUE`."""); `X-Authorization: Bearer $JWT_TOKEN_VALUE`.""");
var requestBody = new RequestBody().description("Login request") var requestBody = new RequestBody().description("Login request")
@ -235,11 +245,12 @@ public class SwaggerConfiguration {
private void addRefreshTokenOperation(OpenAPI openAPI) { private void addRefreshTokenOperation(OpenAPI openAPI) {
var operation = new Operation(); var operation = new Operation();
operation.summary("Refresh user JWT token data"); operation.summary("Refresh user JWT token data");
operation.operationId("refreshToken");
operation.description(""" operation.description("""
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization. The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); `X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
var requestBody = new RequestBody().description("Refresh token request") var requestBody = new RequestBody().description("Refresh token request")
@ -260,7 +271,6 @@ public class SwaggerConfiguration {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
.group(groupName) .group(groupName)
.pathsToMatch(apiPath) .pathsToMatch(apiPath)
.addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer))
.addOperationCustomizer(operationCustomizer()) .addOperationCustomizer(operationCustomizer())
.addOpenApiCustomizer(customOpenApiCustomizer()) .addOpenApiCustomizer(customOpenApiCustomizer())
.build(); .build();
@ -270,9 +280,21 @@ public class SwaggerConfiguration {
@Lazy(false) @Lazy(false)
ModelConverter mapAwareConverter() { ModelConverter mapAwareConverter() {
return (type, context, chain) -> { return (type, context, chain) -> {
JavaType javaType = Json.mapper().constructType(type.getType());
if (javaType != null) {
Class<?> cls = javaType.getRawClass();
Schema<?> atomicSchema = switch (cls.getName()) {
case "java.util.concurrent.atomic.AtomicInteger" -> new IntegerSchema().format("int32");
case "java.util.concurrent.atomic.AtomicLong" -> new IntegerSchema().format("int64");
case "com.google.common.util.concurrent.AtomicDouble" -> new IntegerSchema().format("double");
default -> null;
};
if (atomicSchema != null) {
return atomicSchema;
}
}
if (chain.hasNext()) { if (chain.hasNext()) {
Schema schema = chain.next().resolve(type, context, chain); Schema schema = chain.next().resolve(type, context, chain);
JavaType javaType = Json.mapper().constructType(type.getType());
if (javaType != null) { if (javaType != null) {
Class<?> cls = javaType.getRawClass(); Class<?> cls = javaType.getRawClass();
if (Map.class.isAssignableFrom(cls)) { if (Map.class.isAssignableFrom(cls)) {
@ -282,6 +304,24 @@ public class SwaggerConfiguration {
schema.setProperties(null); schema.setProperties(null);
} }
} }
} else if (schema != null && schema.getProperties() != null && !schema.getProperties().isEmpty()) {
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);
}
} catch (Exception ignored) {
log.trace("Failed to resolve property order for {}", cls.getName(), ignored);
}
} }
} }
return schema; return schema;
@ -292,45 +332,18 @@ public class SwaggerConfiguration {
} }
private void addDefaultSchemas(OpenAPI openAPI) { private void addDefaultSchemas(OpenAPI openAPI) {
var jsonNodeSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(JsonNode.class)).schema; Schema<?> errorCodeSchema = new Schema<>()
jsonNodeSchema.setType("any"); .type("integer")
//noinspection unchecked .description("Platform error code")
jsonNodeSchema.setExamples(List.of(JacksonUtil.newObjectNode())); ._enum(Arrays.stream(ThingsboardErrorCode.values())
jsonNodeSchema.setDescription("A value representing the any type (object or primitive)"); .map(ThingsboardErrorCode::getErrorCode)
.collect(Collectors.toList()));
openAPI.getComponents() openAPI.getComponents()
.addSchemas("JsonNode", jsonNodeSchema)
.addSchemas("LoginRequest", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginRequest.class)).schema) .addSchemas("LoginRequest", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginRequest.class)).schema)
.addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema) .addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema)
.addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema) .addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema)
.addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema); .addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema)
} .addSchemas("ThingsboardErrorCode", errorCodeSchema);
private RouterOperationCustomizer routerOperationCustomizer(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) {
return (routerOperation, handlerMethod) -> {
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new);
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) {
pNames = reflectionParametersNames;
}
MethodParameter[] parameters = handlerMethod.getMethodParameters();
List<String> requestParams = new ArrayList<>();
for (var i = 0; i < parameters.length; i++) {
var methodParameter = parameters[i];
RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
if (requestParam != null) {
String pName = StringUtils.isNotBlank(requestParam.value()) ? requestParam.value() :
pNames[i];
if (StringUtils.isNotBlank(pName)) {
requestParams.add(pName);
}
}
}
if (!requestParams.isEmpty()) {
var path = routerOperation.getPath() + "{?" + String.join(",", requestParams) + "}";
routerOperation.setPath(path);
}
return routerOperation;
};
} }
private OperationCustomizer operationCustomizer() { private OperationCustomizer operationCustomizer() {
@ -370,17 +383,58 @@ public class SwaggerConfiguration {
}); });
sortedPaths.setExtensions(paths.getExtensions()); sortedPaths.setExtensions(paths.getExtensions());
openAPI.setPaths(sortedPaths); openAPI.setPaths(sortedPaths);
if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) {
Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
// Fix all schemas: if they have additionalProperties but no type, set type to object
schemas.forEach((schemaName, schema) -> {
if (schema.getAdditionalProperties() != null && schema.getType() == null) {
schema.setType("object");
log.debug("Added type 'object' to schema: {}", schemaName);
}
});
// Fix polymorphic properties: replace inline oneOf with base type $ref
schemas.values().forEach(schema -> {
replaceInlineOneOfProperties(schema, schemas);
if (schema.getAllOf() != null) {
List<Schema> allOf = schema.getAllOf();
for (Schema allOfElement : allOf) {
replaceInlineOneOfProperties(allOfElement, schemas);
}
}
});
// Fix polymorphic request/response bodies: replace inline oneOf with base type $ref
paths.values().stream()
.flatMap(pathItem -> pathItem.readOperationsMap().values().stream())
.forEach(operation -> {
// Request bodies
if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
replaceInlineOneOfInContent(operation.getRequestBody().getContent(), schemas);
}
// Response bodies
if (operation.getResponses() != null) {
operation.getResponses().values().stream()
.filter(response -> response.getContent() != null)
.forEach(response -> replaceInlineOneOfInContent(response.getContent(), schemas));
}
});
}
// Set JsonNode schema last so model scanning cannot overwrite it
openAPI.getComponents().addSchemas("JsonNode", new Schema<>()
.description("A value representing the any type (object or primitive)")
.example(JacksonUtil.newObjectNode()));
var sortedSchemas = new TreeMap<>(openAPI.getComponents().getSchemas()); var sortedSchemas = new TreeMap<>(openAPI.getComponents().getSchemas());
openAPI.getComponents().setSchemas(new LinkedHashMap<>(sortedSchemas)); openAPI.getComponents().setSchemas(new LinkedHashMap<>(sortedSchemas));
}; };
} }
private SecurityRequirement createSecurityRequirement(String schemeName) { private SecurityRequirement createSecurityRequirement(String schemeName) {
return new SecurityRequirement().addList(schemeName, Arrays.asList( return new SecurityRequirement().addList(schemeName, List.of());
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
} }
private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) { private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) {
@ -388,6 +442,102 @@ public class SwaggerConfiguration {
return tagName != null ? tagFromTagItem(tagName) : null; return tagName != null ? tagFromTagItem(tagName) : null;
} }
private String findBaseTypeForOneOf(Map<String, Schema> schemas, List<Schema> oneOfSchemas) {
if (oneOfSchemas.isEmpty()) {
return null;
}
for (Schema oneOfSchema : oneOfSchemas) {
String ref = oneOfSchema.get$ref();
if (ref == null) {
continue;
}
String refName = ref.substring(ref.lastIndexOf('/') + 1);
// Check if this entry is itself a base type with discriminator
Schema<?> candidate = schemas.get(refName);
if (candidate != null && candidate.getDiscriminator() != null
&& candidate.getDiscriminator().getMapping() != null) {
return refName;
}
// Check if this subtype is in another schema's discriminator mapping
String baseType = schemas.entrySet().stream()
.filter(entry -> {
Schema<?> schema = entry.getValue();
if (schema.getDiscriminator() != null && schema.getDiscriminator().getMapping() != null) {
return schema.getDiscriminator().getMapping().values().stream()
.anyMatch(r -> r.endsWith("/" + refName));
}
return false;
})
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
if (baseType != null) {
return baseType;
}
}
return null;
}
private void replaceInlineOneOfInContent(Content content, Map<String, Schema> schemas) {
content.values().forEach(mediaType -> {
Schema<?> schema = mediaType.getSchema();
if (schema != null && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(schemas, schema.getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
mediaType.setSchema(refSchema);
log.debug("Replaced oneOf in content with $ref to {}", baseType);
}
}
});
}
@SuppressWarnings("unchecked")
private void replaceInlineOneOfProperties(Schema<?> schema, Map<String, Schema> allSchemas) {
if (schema == null || schema.getProperties() == null) {
return;
}
schema.getProperties().forEach((propName, propSchema) -> {
if (propSchema instanceof Schema) {
Schema<?> prop = (Schema<?>) propSchema;
// Check if additionalProperties has oneOf (polymorphic map values)
if (prop.getAdditionalProperties() instanceof Schema) {
Schema<?> additionalProps = (Schema<?>) prop.getAdditionalProperties();
if (additionalProps.getOneOf() != null && !additionalProps.getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
prop.setAdditionalProperties(refSchema);
log.debug("Replaced oneOf in additionalProperties with $ref to {} in property {}", baseType, propName);
}
}
}
// If property has oneOf, try to find the base discriminated type
if (prop.getOneOf() != null && !prop.getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(allSchemas, prop.getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
if (prop.getDescription() != null) {
refSchema.setDescription(prop.getDescription());
}
schema.getProperties().put(propName, refSchema);
log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName);
}
}
}
});
}
private String tagItemFromPathItem(PathItem item) { private String tagItemFromPathItem(PathItem item) {
var operations = item.readOperationsMap().values(); var operations = item.readOperationsMap().values();
var operation = operations.stream().findAny(); var operation = operations.stream().findAny();
@ -480,28 +630,34 @@ public class SwaggerConfiguration {
private static ApiResponses loginErrorResponses() { private static ApiResponses loginErrorResponses() {
ApiResponses apiResponses = new ApiResponses(); ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse("401", errorResponse("Unauthorized", Map<String, Example> unauthorizedExamples = new LinkedHashMap<>();
Map.of(
"bad-credentials", errorExample("Bad credentials", unauthorizedExamples.put("bad-credentials", errorExample("Bad credentials",
ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
"token-expired", errorExample("JWT token expired",
ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)), unauthorizedExamples.put("token-expired", errorExample("JWT token expired",
"account-disabled", errorExample("Disabled account", ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)));
ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
"account-locked", errorExample("Locked account", unauthorizedExamples.put("account-disabled", errorExample("Disabled account",
ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
"authentication-failed", errorExample("General authentication error",
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) unauthorizedExamples.put("account-locked", errorExample("Locked account",
) ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
));
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); unauthorizedExamples.put("authentication-failed", errorExample("General authentication error",
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)", ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
Map.of(
"credentials-expired", errorExample("Expired credentials", unauthorizedExamples.put("credentials-expired", errorExample("Expired credentials",
ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30))) ThingsboardCredentialsExpiredResponse.of("User password expired!", "udgDQOpS1Q4ZFEL8qHF9s8cSKQ7d1h")));
),
credentialsExpiredSchema Schema<? extends ThingsboardErrorResponse> unauthorizedSchema = new Schema<>();
unauthorizedSchema.oneOf(List.of(
new Schema<ThingsboardErrorResponse>().$ref("#/components/schemas/ThingsboardErrorResponse"),
new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse")
)); ));
apiResponses.addApiResponse("401", errorResponse("Unauthorized", unauthorizedExamples, unauthorizedSchema));
return apiResponses; return apiResponses;
} }
@ -520,10 +676,43 @@ public class SwaggerConfiguration {
return new ApiResponse().description(description).content(content); return new ApiResponse().description(description).content(content);
} }
private static List<String> resolvePropertyOrder(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
// 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) {
fieldToJsonName.put(prop.getField().getName(), prop.getName());
} else {
getterOnlyNames.add(prop.getName());
}
}
// Walk class hierarchy (superclass first) to get field declaration order
List<Class<?>> hierarchy = new ArrayList<>();
for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
hierarchy.add(0, c);
}
List<String> ordered = new ArrayList<>();
for (Class<?> c : hierarchy) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
String jsonName = fieldToJsonName.get(f.getName());
if (jsonName != null) ordered.add(jsonName);
}
}
// Append getter-only properties (no backing field) at the end
ordered.addAll(getterOnlyNames);
return ordered;
}
private static Example errorExample(String summary, ThingsboardErrorResponse example) { private static Example errorExample(String summary, ThingsboardErrorResponse example) {
var node = (ObjectNode) JacksonUtil.valueToTree(example);
node.put("timestamp", 1609459200000L);
return new Example() return new Example()
.summary(summary) .summary(summary)
.value(example); .value(node);
} }
} }

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

@ -17,6 +17,7 @@ package org.thingsboard.server.config;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHandler;
@ -40,11 +41,16 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
private final WebSocketHandler wsHandler; private final WebSocketHandler wsHandler;
@Value("${server.ws.max_text_message_buffer_size:32768}")
private int maxTextMessageBufferSize;
@Value("${server.ws.max_binary_message_buffer_size:32768}")
private int maxBinaryMessageBufferSize;
@Bean @Bean
public ServletServerContainerFactoryBean createWebSocketContainer() { public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(32768); container.setMaxTextMessageBufferSize(maxTextMessageBufferSize);
container.setMaxBinaryMessageBufferSize(32768); container.setMaxBinaryMessageBufferSize(maxBinaryMessageBufferSize);
return container; return container;
} }
@ -54,7 +60,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
} }
registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*"); registry.addHandler(wsHandler, WS_API_MAPPING)
.setAllowedOriginPatterns("*");
} }
} }

4
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -417,7 +417,7 @@ public class AdminController extends BaseController {
"provider sends authorization code to specified redirect uri.)") "provider sends authorization code to specified redirect uri.)")
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/mail/oauth2/authorize", produces = "application/text") @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text")
public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { public String getMailOAuth2AuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
String state = StringUtils.generateSafeToken(); String state = StringUtils.generateSafeToken();
if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) {
CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180); CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180);
@ -442,7 +442,7 @@ public class AdminController extends BaseController {
} }
@GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"})
public void codeProcessingUrl( public void handleMailOAuth2Callback(
@RequestParam(value = "code") String code, @RequestParam(value = "state") String state, @RequestParam(value = "code") String code, @RequestParam(value = "state") String state,
HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException {
Optional<Cookie> prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME); Optional<Cookie> prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME);

4
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -227,12 +227,12 @@ public class AlarmController extends BaseController {
return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser()); return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser());
} }
@ApiOperation(value = "Get Alarms (getAlarms)", @ApiOperation(value = "Get Alarms (getAlarmsByEntity)",
notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/alarm/{entityType}/{entityId}") @GetMapping(value = "/alarm/{entityType}/{entityId}")
public PageData<AlarmInfo> getAlarms( public PageData<AlarmInfo> getAlarmsByEntity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)

42
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -23,6 +24,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -217,8 +219,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets owned by tenant. " + notes = "Returns a page of assets owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/assets")
@ResponseBody
public PageData<Asset> getTenantAssets( public PageData<Asset> getTenantAssets(
@Parameter(description = PAGE_SIZE_DESCRIPTION) @Parameter(description = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -245,8 +246,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets info objects owned by tenant. " + notes = "Returns a page of assets info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/assetInfos")
@ResponseBody
public PageData<AssetInfo> getTenantAssetInfos( public PageData<AssetInfo> getTenantAssetInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION) @Parameter(description = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -274,25 +274,30 @@ public class AssetController extends BaseController {
} }
} }
@ApiOperation(value = "Get Tenant Asset (getTenantAsset)", @Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/assets", params = {"assetName"})
public Asset getTenantAsset(@RequestParam String assetName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName));
}
@ApiOperation(value = "Get Tenant Asset (getTenantAssetByName)",
notes = "Requested asset must be owned by tenant that the user belongs to. " + notes = "Requested asset must be owned by tenant that the user belongs to. " +
"Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH) "Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/asset")
@ResponseBody public Asset getTenantAssetByName(
public Asset getTenantAsset(
@Parameter(description = ASSET_NAME_DESCRIPTION) @Parameter(description = ASSET_NAME_DESCRIPTION)
@RequestParam String assetName) throws ThingsboardException { @RequestParam String assetName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); return getTenantAsset(assetName);
return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName));
} }
@ApiOperation(value = "Get Customer Assets (getCustomerAssets)", @ApiOperation(value = "Get Customer Assets (getCustomerAssets)",
notes = "Returns a page of assets objects assigned to customer. " + notes = "Returns a page of assets objects assigned to customer. " +
PAGE_DATA_PARAMETERS) PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/assets")
@ResponseBody
public PageData<Asset> getCustomerAssets( public PageData<Asset> getCustomerAssets(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@ -324,8 +329,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets info objects assigned to customer. " + notes = "Returns a page of assets info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION) PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/assetInfos")
@ResponseBody
public PageData<AssetInfo> getCustomerAssetInfos( public PageData<AssetInfo> getCustomerAssetInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@ -361,8 +365,7 @@ public class AssetController extends BaseController {
@ApiOperation(value = "Get Assets By Ids (getAssetsByIds)", @ApiOperation(value = "Get Assets By Ids (getAssetsByIds)",
notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ") notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) @GetMapping(value = "/assets")
@ResponseBody
public List<Asset> getAssetsByIds( public List<Asset> getAssetsByIds(
@Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException { @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException {
@ -383,14 +386,14 @@ public class AssetController extends BaseController {
return checkNotNull(assets.get()); return checkNotNull(assets.get());
} }
@ApiOperation(value = "Find related assets (findByQuery)", @ApiOperation(value = "Find related assets (findAssetsByQuery)",
notes = "Returns all assets that are related to the specific entity. " + notes = "Returns all assets that are related to the specific entity. " +
"The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " + "The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info.") "See 'Model' tab of the Parameters for more info.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assets", method = RequestMethod.POST) @RequestMapping(value = "/assets", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public List<Asset> findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { public List<Asset> findAssetsByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query); checkNotNull(query);
checkNotNull(query.getParameters()); checkNotNull(query.getParameters());
checkNotNull(query.getAssetTypes()); checkNotNull(query.getAssetTypes());
@ -469,8 +472,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets assigned to edge. " + notes = "Returns a page of assets assigned to edge. " +
PAGE_DATA_PARAMETERS) PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/edge/{edgeId}/assets")
@ResponseBody
public PageData<Asset> getEdgeAssets( public PageData<Asset> getEdgeAssets(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,

24
application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -175,8 +176,7 @@ public class AssetProfileController extends BaseController {
notes = "Returns a page of asset profile objects owned by tenant. " + notes = "Returns a page of asset profile objects owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/assetProfiles")
@ResponseBody
public PageData<AssetProfile> getAssetProfiles( public PageData<AssetProfile> getAssetProfiles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -196,8 +196,7 @@ public class AssetProfileController extends BaseController {
notes = "Returns a page of asset profile info objects owned by tenant. " + notes = "Returns a page of asset profile info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/assetProfileInfos")
@ResponseBody
public PageData<AssetProfileInfo> getAssetProfileInfos( public PageData<AssetProfileInfo> getAssetProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -227,12 +226,10 @@ public class AssetProfileController extends BaseController {
return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly)); return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly));
} }
@ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)", @Hidden
notes = "Requested asset profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"}) @GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"})
public List<AssetProfileInfo> getAssetProfilesByIds( public List<AssetProfileInfo> getAssetProfilesByIdsV1(
@Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("assetProfileIds") Set<UUID> assetProfileUUIDs) throws ThingsboardException { @RequestParam("assetProfileIds") Set<UUID> assetProfileUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
@ -243,4 +240,15 @@ public class AssetProfileController extends BaseController {
return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds); return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds);
} }
@ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)",
notes = "Requested asset profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/assetProfileInfos/list")
public List<AssetProfileInfo> getAssetProfilesByIds(
@Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("assetProfileIds") Set<UUID> assetProfileUUIDs) throws ThingsboardException {
return getAssetProfilesByIdsV1(assetProfileUUIDs);
}
} }

15
application/src/main/java/org/thingsboard/server/controller/AuditLogController.java

@ -18,11 +18,10 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -74,8 +73,7 @@ public class AuditLogController extends BaseController {
"and users actions (login, logout, etc.) that belong to this customer. " + "and users actions (login, logout, etc.) that belong to this customer. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/audit/logs/customer/{customerId}")
@ResponseBody
public PageData<AuditLog> getAuditLogsByCustomerId( public PageData<AuditLog> getAuditLogsByCustomerId(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@ -107,8 +105,7 @@ public class AuditLogController extends BaseController {
"For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " + "For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/audit/logs/user/{userId}")
@ResponseBody
public PageData<AuditLog> getAuditLogsByUserId( public PageData<AuditLog> getAuditLogsByUserId(
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable("userId") String strUserId, @PathVariable("userId") String strUserId,
@ -141,8 +138,7 @@ public class AuditLogController extends BaseController {
"For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/audit/logs/entity/{entityType}/{entityId}")
@ResponseBody
public PageData<AuditLog> getAuditLogsByEntityId( public PageData<AuditLog> getAuditLogsByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String strEntityType, @PathVariable("entityType") String strEntityType,
@ -176,8 +172,7 @@ public class AuditLogController extends BaseController {
notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/audit/logs")
@ResponseBody
public PageData<AuditLog> getAuditLogs( public PageData<AuditLog> getAuditLogs(
@Parameter(description = PAGE_SIZE_DESCRIPTION) @Parameter(description = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize, @RequestParam int pageSize,

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

@ -136,7 +136,7 @@ public class AuthController extends BaseController {
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " + "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'. " + "If token is not valid, returns '409 Conflict'. " +
"If token is expired, redirects to error page.") "If token is expired, redirects to error page.")
@GetMapping(value = "/noauth/activate", params = {"activateToken"}) @GetMapping(value = "/noauth/activate")
public ResponseEntity<?> checkActivateToken( public ResponseEntity<?> checkActivateToken(
@Parameter(description = "The activate token string.") @Parameter(description = "The activate token string.")
@RequestParam(value = "activateToken") String activateToken) { @RequestParam(value = "activateToken") String activateToken) {
@ -176,7 +176,7 @@ public class AuthController extends BaseController {
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " + "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'. " + "If token is not valid, returns '409 Conflict'. " +
"If token is expired, redirects to error page.") "If token is expired, redirects to error page.")
@GetMapping(value = "/noauth/resetPassword", params = {"resetToken"}) @GetMapping(value = "/noauth/resetPassword")
public ResponseEntity<?> checkResetToken( public ResponseEntity<?> checkResetToken(
@Parameter(description = "The reset token string.") @Parameter(description = "The reset token string.")
@RequestParam(value = "resetToken") String resetToken) { @RequestParam(value = "resetToken") String resetToken) {

44
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -17,7 +17,11 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -163,26 +167,16 @@ public class CalculatedFieldController extends BaseController {
return calculatedField; return calculatedField;
} }
@ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)", @Hidden
notes = "Fetch the Calculated Fields based on the provided Entity Id."
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"})
public PageData<CalculatedField> getCalculatedFieldsByEntityId(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) public PageData<CalculatedField> getCalculatedFieldsByEntityIdV1(@PathVariable("entityType") String entityType,
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@Parameter(description = "Calculated field type. If not specified, all types will be returned.")
@RequestParam(required = false) CalculatedFieldType type, @RequestParam(required = false) CalculatedFieldType type,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"}))
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
checkParameter("entityId", entityIdStr); checkParameter("entityId", entityIdStr);
@ -191,8 +185,29 @@ public class CalculatedFieldController extends BaseController {
return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink)); return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink));
} }
@ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)",
notes = "Fetch the Calculated Fields based on the provided Entity Id."
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/calculatedField/{entityType}/{entityId}")
public PageData<CalculatedField> getCalculatedFieldsByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page,
@Parameter(description = "Calculated field type. If not specified, all types will be returned.")
@RequestParam(required = false) CalculatedFieldType type,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException {
return getCalculatedFieldsByEntityIdV1(entityType, entityIdStr, pageSize, page, type, textSearch, sortProperty, sortOrder);
}
@ApiOperation(value = "Get calculated fields (getCalculatedFields)", @ApiOperation(value = "Get calculated fields (getCalculatedFields)",
notes = "Fetch tenant calculated fields based on the filter.") notes = "Fetch tenant calculated fields based on the filter.")
@Parameters({
@Parameter(name = "name", description = "Repeatable name query parameter", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/calculatedFields") @GetMapping(value = "/calculatedFields")
public PageData<CalculatedFieldInfo> getCalculatedFields(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<CalculatedFieldInfo> getCalculatedFields(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -205,14 +220,13 @@ public class CalculatedFieldController extends BaseController {
@RequestParam(required = false) EntityType entityType, @RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.") @Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.")
@RequestParam(required = false) Set<UUID> entities, @RequestParam(required = false) Set<UUID> entities,
@Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2")
@RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma)
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"}))
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -289,7 +303,7 @@ public class CalculatedFieldController extends BaseController {
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/calculatedField/testScript") @PostMapping("/calculatedField/testScript")
public JsonNode testScript( public JsonNode testCalculatedFieldScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.")
@RequestBody JsonNode inputParams) { @RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText(); String expression = inputParams.get("expression").asText();

4
application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java

@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -81,8 +82,7 @@ public class ComponentDescriptorController extends BaseController {
notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " + notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " +
COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) @GetMapping(value = "/components")
@ResponseBody
public List<ComponentDescriptor> getComponentDescriptorsByTypes( public List<ComponentDescriptor> getComponentDescriptorsByTypes(
@Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("componentTypes") String[] strComponentTypes, @RequestParam("componentTypes") String[] strComponentTypes,

24
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -172,8 +173,7 @@ public class CustomerController extends BaseController {
notes = "Returns a page of customers owned by tenant. " + notes = "Returns a page of customers owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customers")
@ResponseBody
public PageData<Customer> getCustomers( public PageData<Customer> getCustomers(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -193,8 +193,7 @@ public class CustomerController extends BaseController {
@ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)",
notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/customers")
@ResponseBody
public Customer getTenantCustomer( public Customer getTenantCustomer(
@Parameter(description = "A string value representing the Customer title.") @Parameter(description = "A string value representing the Customer title.")
@RequestParam String customerTitle) throws ThingsboardException { @RequestParam String customerTitle) throws ThingsboardException {
@ -202,12 +201,10 @@ public class CustomerController extends BaseController {
return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found");
} }
@ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)", @Hidden
notes = "Returns a list of Customer objects based on the provided ids." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customers", params = {"customerIds"}) @GetMapping(value = "/customers", params = {"customerIds"})
public List<Customer> getCustomersByIds( public List<Customer> getCustomersByIdsV1(
@Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("customerIds") Set<UUID> customerUUIDs) throws ThingsboardException { @RequestParam("customerIds") Set<UUID> customerUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
@ -218,4 +215,15 @@ public class CustomerController extends BaseController {
return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds); return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds);
} }
@ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)",
notes = "Returns a list of Customer objects based on the provided ids." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customers/list")
public List<Customer> getCustomersByIds(
@Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("customerIds") Set<UUID> customerUUIDs) throws ThingsboardException {
return getCustomersByIdsV1(customerUUIDs);
}
} }

37
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -17,10 +17,10 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -119,7 +119,7 @@ public class DashboardController extends BaseController {
"Used to adjust view of the dashboards according to the difference between browser and server time.") "Used to adjust view of the dashboards according to the difference between browser and server time.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/serverTime") @GetMapping(value = "/dashboard/serverTime")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137"))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "1636023857137")))
public long getServerTime() { public long getServerTime() {
return System.currentTimeMillis(); return System.currentTimeMillis();
} }
@ -131,7 +131,7 @@ public class DashboardController extends BaseController {
"The actual value of the limit is configurable in the system configuration file.") "The actual value of the limit is configurable in the system configuration file.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/maxDatapointsLimit") @GetMapping(value = "/dashboard/maxDatapointsLimit")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000"))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "5000")))
public long getMaxDatapointsLimit() { public long getMaxDatapointsLimit() {
return maxDatapointsLimit; return maxDatapointsLimit;
} }
@ -176,6 +176,8 @@ public class DashboardController extends BaseController {
"Referencing non-existing dashboard Id will cause 'Not Found' error. " + "Referencing non-existing dashboard Id will cause 'Not Found' error. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = Dashboard.class)))
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping(value = "/dashboard") @PostMapping(value = "/dashboard")
public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.") public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
@ -325,12 +327,12 @@ public class DashboardController extends BaseController {
return tbDashboardService.unassignDashboardFromPublicCustomer(dashboard, getCurrentUser()); return tbDashboardService.unassignDashboardFromPublicCustomer(dashboard, getCurrentUser());
} }
@ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)", @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboardsByTenantId)",
notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS +
SYSTEM_AUTHORITY_PARAGRAPH) SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}) @GetMapping(value = "/tenant/{tenantId}/dashboards")
public PageData<DashboardInfo> getTenantDashboards( public PageData<DashboardInfo> getTenantDashboardsByTenantId(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId, @PathVariable(TENANT_ID) String strTenantId,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -353,7 +355,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by the tenant of a current user. " notes = "Returns a page of dashboard info objects owned by the tenant of a current user. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}) @GetMapping(value = "/tenant/dashboards")
public PageData<DashboardInfo> getTenantDashboards( public PageData<DashboardInfo> getTenantDashboards(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -380,7 +382,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by the specified customer. " notes = "Returns a page of dashboard info objects owned by the specified customer. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}) @GetMapping(value = "/customer/{customerId}/dashboards")
public PageData<DashboardInfo> getCustomerDashboards( public PageData<DashboardInfo> getCustomerDashboards(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -574,7 +576,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects assigned to the specified edge. " notes = "Returns a page of dashboard info objects assigned to the specified edge. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}) @GetMapping(value = "/edge/{edgeId}/dashboards")
public PageData<DashboardInfo> getEdgeDashboards( public PageData<DashboardInfo> getEdgeDashboards(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@ -602,13 +604,10 @@ public class DashboardController extends BaseController {
return checkNotNull(filteredResult); return checkNotNull(filteredResult);
} }
@ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)", @Hidden
notes = "Returns a list of DashboardInfo objects based on the provided ids. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboards", params = {"dashboardIds"}) @GetMapping(value = "/dashboards", params = {"dashboardIds"})
public List<DashboardInfo> getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) public List<DashboardInfo> getDashboardsByIdsV1(@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<DashboardId> dashboardIds = new ArrayList<>(); List<DashboardId> dashboardIds = new ArrayList<>();
for (UUID dashboardUUID : dashboardUUIDs) { for (UUID dashboardUUID : dashboardUUIDs) {
@ -618,6 +617,16 @@ public class DashboardController extends BaseController {
return filterDashboardsByReadPermission(dashboards); return filterDashboardsByReadPermission(dashboards);
} }
@ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)",
notes = "Returns a list of DashboardInfo objects based on the provided ids. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboards/list")
public List<DashboardInfo> getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
return getDashboardsByIdsV1(dashboardUUIDs);
}
private Set<CustomerId> customerIdFromStr(String[] strCustomerIds) { private Set<CustomerId> customerIdFromStr(String[] strCustomerIds) {
Set<CustomerId> customerIds = new HashSet<>(); Set<CustomerId> customerIds = new HashSet<>();
if (strCustomerIds != null) { if (strCustomerIds != null) {

28
application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -75,6 +76,7 @@ public class DeviceConnectivityController extends BaseController {
description = "OK", description = "OK",
content = @Content( content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = JsonNode.class),
examples = { examples = {
@ExampleObject( @ExampleObject(
name = "http", name = "http",
@ -105,7 +107,18 @@ public class DeviceConnectivityController extends BaseController {
return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device); return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device);
} }
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.") @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)",
notes = "Download server certificate.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OK",
content = @Content(
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
schema = @Schema(type = "string", format = "binary")
)
)
})
@RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET) @RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadServerCertificate(@Parameter(description = PROTOCOL_PARAM_DESCRIPTION) public ResponseEntity<org.springframework.core.io.Resource> downloadServerCertificate(@Parameter(description = PROTOCOL_PARAM_DESCRIPTION)
@ -122,7 +135,18 @@ public class DeviceConnectivityController extends BaseController {
.body(pemCert); .body(pemCert);
} }
@ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)", notes = "Download generated docker-compose.yml for gateway.") @ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)",
notes = "Download generated docker-compose.yml for gateway.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OK",
content = @Content(
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
schema = @Schema(type = "string", format = "binary")
)
)
})
@RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}/docker-compose/download", method = RequestMethod.GET) @RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}/docker-compose/download", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadGatewayDockerCompose(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) public ResponseEntity<org.springframework.core.io.Resource> downloadGatewayDockerCompose(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)

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

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -29,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -350,8 +352,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices owned by tenant. " + notes = "Returns a page of devices owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/devices")
@ResponseBody
public PageData<Device> getTenantDevices( public PageData<Device> getTenantDevices(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -378,8 +379,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices info objects owned by tenant. " + notes = "Returns a page of devices info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/deviceInfos")
@ResponseBody
public PageData<DeviceInfo> getTenantDeviceInfos( public PageData<DeviceInfo> getTenantDeviceInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -411,25 +411,31 @@ public class DeviceController extends BaseController {
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
} }
@ApiOperation(value = "Get Tenant Device (getTenantDevice)", @Hidden
notes = "Requested device must be owned by tenant that the user belongs to. " +
"Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/devices", params = {"deviceName"})
@ResponseBody
public Device getTenantDevice( public Device getTenantDevice(
@Parameter(description = DEVICE_NAME_DESCRIPTION)
@RequestParam String deviceName) throws ThingsboardException { @RequestParam String deviceName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName));
} }
@ApiOperation(value = "Get Tenant Device (getTenantDeviceByName)",
notes = "Requested device must be owned by tenant that the user belongs to. " +
"Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/device")
public Device getTenantDeviceByName(
@Parameter(description = DEVICE_NAME_DESCRIPTION)
@RequestParam String deviceName) throws ThingsboardException {
return getTenantDevice(deviceName);
}
@ApiOperation(value = "Get Customer Devices (getCustomerDevices)", @ApiOperation(value = "Get Customer Devices (getCustomerDevices)",
notes = "Returns a page of devices objects assigned to customer. " + notes = "Returns a page of devices objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/devices")
@ResponseBody
public PageData<Device> getCustomerDevices( public PageData<Device> getCustomerDevices(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -461,8 +467,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices info objects assigned to customer. " + notes = "Returns a page of devices info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/deviceInfos")
@ResponseBody
public PageData<DeviceInfo> getCustomerDeviceInfos( public PageData<DeviceInfo> getCustomerDeviceInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@ -502,8 +507,7 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)",
notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) @GetMapping(value = "/devices")
@ResponseBody
public List<Device> getDevicesByIds( public List<Device> getDevicesByIds(
@Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException {
@ -524,14 +528,14 @@ public class DeviceController extends BaseController {
return checkNotNull(devices.get()); return checkNotNull(devices.get());
} }
@ApiOperation(value = "Find related devices (findByQuery)", @ApiOperation(value = "Find related devices (findDevicesByQuery)",
notes = "Returns all devices that are related to the specific entity. " + notes = "Returns all devices that are related to the specific entity. " +
"The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " + "The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", method = RequestMethod.POST) @RequestMapping(value = "/devices", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public List<Device> findByQuery( public List<Device> findDevicesByQuery(
@Parameter(description = "The device search query JSON") @Parameter(description = "The device search query JSON")
@RequestBody DeviceSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { @RequestBody DeviceSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query); checkNotNull(query);
@ -730,8 +734,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices assigned to edge. " + notes = "Returns a page of devices assigned to edge. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/edge/{edgeId}/devices")
@ResponseBody
public PageData<DeviceInfo> getEdgeDevices( public PageData<DeviceInfo> getEdgeDevices(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,

30
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -129,7 +130,7 @@ public class DeviceProfileController extends BaseController {
return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId()));
} }
@ApiOperation(value = "Get time series keys (getTimeseriesKeys)", @ApiOperation(value = "Get time series keys (getDeviceProfileTimeseriesKeys)",
notes = "Get a set of unique time series keys used by devices that belong to specified profile. " + notes = "Get a set of unique time series keys used by devices that belong to specified profile. " +
"If profile is not set returns a list of unique keys among all profiles. " + "If profile is not set returns a list of unique keys among all profiles. " +
"The call is used for auto-complete in the UI forms. " + "The call is used for auto-complete in the UI forms. " +
@ -138,7 +139,7 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET) @RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public List<String> getTimeseriesKeys( public List<String> getDeviceProfileTimeseriesKeys(
@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException { @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
DeviceProfileId deviceProfileId; DeviceProfileId deviceProfileId;
@ -228,8 +229,7 @@ public class DeviceProfileController extends BaseController {
notes = "Returns a page of devices profile objects owned by tenant. " + notes = "Returns a page of devices profile objects owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/deviceProfiles")
@ResponseBody
public PageData<DeviceProfile> getDeviceProfiles( public PageData<DeviceProfile> getDeviceProfiles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -249,8 +249,7 @@ public class DeviceProfileController extends BaseController {
notes = "Returns a page of devices profile info objects owned by tenant. " + notes = "Returns a page of devices profile info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/deviceProfileInfos")
@ResponseBody
public PageData<DeviceProfileInfo> getDeviceProfileInfos( public PageData<DeviceProfileInfo> getDeviceProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -282,14 +281,10 @@ public class DeviceProfileController extends BaseController {
return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly)); return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly));
} }
@ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfilesByIds)", @Hidden
notes = "Requested device profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"}) @GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"})
public List<DeviceProfileInfo> getDeviceProfileInfosByIds( public List<DeviceProfileInfo> getDeviceProfileInfosByIdsV1(@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
@Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<DeviceProfileId> deviceProfileIds = new ArrayList<>(); List<DeviceProfileId> deviceProfileIds = new ArrayList<>();
for (UUID deviceProfileUUID : deviceProfileUUIDs) { for (UUID deviceProfileUUID : deviceProfileUUIDs) {
@ -298,4 +293,15 @@ public class DeviceProfileController extends BaseController {
return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds); return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds);
} }
@ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfileInfosByIds)",
notes = "Requested device profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/deviceProfileInfos/list")
public List<DeviceProfileInfo> getDeviceProfileInfosByIds(
@Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
return getDeviceProfileInfosByIdsV1(deviceProfileUUIDs);
}
} }

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

@ -81,12 +81,12 @@ public class DomainController extends BaseController {
return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser()); return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser());
} }
@ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", @ApiOperation(value = "Update oauth2 clients (updateDomainOauth2Clients)",
notes = "Update oauth2 clients for the specified domain. ") notes = "Update oauth2 clients for the specified domain. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PutMapping(value = "/domain/{id}/oauth2Clients") @PutMapping(value = "/domain/{id}/oauth2Clients")
public void updateOauth2Clients(@PathVariable UUID id, public void updateDomainOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException { @RequestBody UUID[] clientIds) throws ThingsboardException {
DomainId domainId = new DomainId(id); DomainId domainId = new DomainId(id);
Domain domain = checkDomainId(domainId, Operation.WRITE); Domain domain = checkDomainId(domainId, Operation.WRITE);
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds); List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);

42
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -277,7 +278,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges info objects owned by tenant. " + notes = "Returns a page of edges info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}) @GetMapping(value = "/tenant/edgeInfos")
public PageData<EdgeInfo> getTenantEdgeInfos( public PageData<EdgeInfo> getTenantEdgeInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -300,17 +301,24 @@ public class EdgeController extends BaseController {
} }
} }
@ApiOperation(value = "Get Tenant Edge (getTenantEdge)", @Hidden
notes = "Requested edge must be owned by tenant or customer that the user belongs to. " +
"Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/edges", params = {"edgeName"}) @GetMapping(value = "/tenant/edges", params = {"edgeName"})
public Edge getTenantEdge(@Parameter(description = "Unique name of the edge", required = true) public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException {
@RequestParam String edgeName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName));
} }
@ApiOperation(value = "Get Tenant Edge by name (getTenantEdgeByName)",
notes = "Requested edge must be owned by tenant or customer that the user belongs to. " +
"Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/edge")
public Edge getTenantEdgeByName(@Parameter(description = "Unique name of the edge", required = true)
@RequestParam String edgeName) throws ThingsboardException {
return getTenantEdge(edgeName);
}
@ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)", @ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)",
notes = "Change root rule chain of the edge to the new provided rule chain. \n" + notes = "Change root rule chain of the edge to the new provided rule chain. \n" +
"This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH) "This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH)
@ -334,7 +342,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges objects assigned to customer. " + notes = "Returns a page of edges objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}) @GetMapping(value = "/customer/{customerId}/edges")
public PageData<Edge> getCustomerEdges( public PageData<Edge> getCustomerEdges(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@ -369,7 +377,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges info objects assigned to customer. " + notes = "Returns a page of edges info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}) @GetMapping(value = "/customer/{customerId}/edgeInfos")
public PageData<EdgeInfo> getCustomerEdgeInfos( public PageData<EdgeInfo> getCustomerEdgeInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId, @PathVariable("customerId") String strCustomerId,
@ -400,12 +408,10 @@ public class EdgeController extends BaseController {
return checkNotNull(result); return checkNotNull(result);
} }
@ApiOperation(value = "Get Edges By Ids (getEdgesByIds)", @Hidden
notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edges", params = {"edgeIds"}) @GetMapping(value = "/edges", params = {"edgeIds"})
public List<Edge> getEdgesByIds( public List<Edge> getEdgesByIds(
@Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException {
checkArrayParameter("edgeIds", strEdgeIds); checkArrayParameter("edgeIds", strEdgeIds);
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -425,13 +431,23 @@ public class EdgeController extends BaseController {
return checkNotNull(edges); return checkNotNull(edges);
} }
@ApiOperation(value = "Find related edges (findByQuery)", @ApiOperation(value = "Get Edges By Ids (getEdgeList)",
notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edges/list")
public List<Edge> getEdgeList(
@Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException {
return getEdgesByIds(strEdgeIds);
}
@ApiOperation(value = "Find related edges (findEdgesByQuery)",
notes = "Returns all edges that are related to the specific entity. " + notes = "Returns all edges that are related to the specific entity. " +
"The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " + "The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/edges") @PostMapping(value = "/edges")
public List<Edge> findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { public List<Edge> findEdgesByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query); checkNotNull(query);
checkNotNull(query.getParameters()); checkNotNull(query.getParameters());
checkNotNull(query.getEdgeTypes()); checkNotNull(query.getEdgeTypes());

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

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -234,7 +235,7 @@ public class EntitiesVersionControlController extends BaseController {
"}" + "}" +
MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"}) @GetMapping(value = "/version/{entityType}/{externalEntityUuid}")
public DeferredResult<PageData<EntityVersion>> listEntityVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) public DeferredResult<PageData<EntityVersion>> listEntityVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable EntityType entityType, @PathVariable EntityType entityType,
@Parameter(description = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.") @Parameter(description = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.")
@ -263,7 +264,7 @@ public class EntitiesVersionControlController extends BaseController {
"If specified branch does not exist - empty page data will be returned. " + "If specified branch does not exist - empty page data will be returned. " +
"The response structure is the same as for `listEntityVersions` API method." + "The response structure is the same as for `listEntityVersions` API method." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"}) @GetMapping(value = "/version/{entityType}")
public DeferredResult<PageData<EntityVersion>> listEntityTypeVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) public DeferredResult<PageData<EntityVersion>> listEntityTypeVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable EntityType entityType, @PathVariable EntityType entityType,
@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true)
@ -288,7 +289,7 @@ public class EntitiesVersionControlController extends BaseController {
"If specified branch does not exist - empty page data will be returned. " + "If specified branch does not exist - empty page data will be returned. " +
"The response format is the same as for `listEntityVersions` API method." + "The response format is the same as for `listEntityVersions` API method." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version", params = {"branch", "pageSize", "page"}) @GetMapping(value = "/version")
public DeferredResult<PageData<EntityVersion>> listVersions(@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) public DeferredResult<PageData<EntityVersion>> listVersions(@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true)
@RequestParam String branch, @RequestParam String branch,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -355,7 +356,7 @@ public class EntitiesVersionControlController extends BaseController {
"Returns an object with current entity data and the one at a specific version. " + "Returns an object with current entity data and the one at a specific version. " +
"Entity data structure is the same as stored in a repository. " + "Entity data structure is the same as stored in a repository. " +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"versionId"}) @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}")
public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable EntityType entityType, @PathVariable EntityType entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)

58
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -47,6 +48,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import java.util.Set;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
@ -115,9 +118,11 @@ public class EntityQueryController extends BaseController {
return entityQueryService.countAlarmsByQuery(getCurrentUser(), query); return entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
} }
@Deprecated(forRemoval = true)
@ApiOperation( @ApiOperation(
value = "Find Available Entity Keys by Query", value = "Find Available Entity Keys by Query (deprecated)",
notes = """ notes = """
**Deprecated.** Use the V2 endpoint (`POST /api/v2/entitiesQuery/find/keys`) instead.\n
Returns unique time series and/or attribute key names from entities matching the query.\n Returns unique time series and/or attribute key names from entities matching the query.\n
Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n
Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
@ -128,9 +133,6 @@ public class EntityQueryController extends BaseController {
@Parameter(description = "Entity data query to find entities. Page size is capped at 100.") @Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
@RequestBody EntityDataQuery query, @RequestBody EntityDataQuery query,
// fixme: combination of timeseries = false and attributes = false is allowed, but always results in empty response, therefore does not make any sense
// such combinations should NOT be allowed, but changing this will break clients
@Parameter(description = """ @Parameter(description = """
When true, includes unique time series key names in the response. When true, includes unique time series key names in the response.
When false, the 'timeseries' list will be empty.""") When false, the 'timeseries' list will be empty.""")
@ -155,6 +157,54 @@ public class EntityQueryController extends BaseController {
return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope)); return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope));
} }
@ApiOperation(
value = "Find Available Entity Keys By Query",
notes = """
Discovers unique time series and/or attribute key names available on entities that match the given query.
Works in two steps: first, the request body (an Entity Data Query) is executed to find matching entities
(page size is capped at 100); then, all distinct key names are collected from those entities.\n
Optionally, each key can include a sample — the most recent value (by timestamp) for that key
across all matched entities."""
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/v2/entitiesQuery/find/keys")
public DeferredResult<AvailableEntityKeysV2> findAvailableEntityKeysByQueryV2(
@Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
@RequestBody EntityDataQuery query,
@Parameter(description = """
When true, includes unique time series keys in the response.
When false, the 'timeseries' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""")
@RequestParam(defaultValue = "true") boolean includeTimeseries,
@Parameter(description = """
When true, includes unique attribute keys in the response.
When false, the 'attributes' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""")
@RequestParam(defaultValue = "true") boolean includeAttributes,
@Parameter(description = """
Filters attribute keys by scope. Only applies when 'includeAttributes' is true.
When not specified, scopes are auto-determined: all three scopes (server, client, shared) for device entities,
server scope only for other entity types.""",
schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
@RequestParam(required = false) Set<AttributeScope> scopes,
@Parameter(description = """
When true, each key entry includes a 'sample' object with the most recent value and timestamp.
When false, only key names are returned (sample is omitted from JSON).""")
@RequestParam(defaultValue = "false") boolean includeSamples
) throws ThingsboardException {
resolveQuery(query);
EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE);
}
return wrapFuture(entityQueryService.findAvailableEntityKeysByQuery(
getCurrentUser(), query,
includeTimeseries, includeAttributes, scopes, includeSamples));
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping("/edqs/system/request") @PostMapping("/edqs/system/request")
public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) { public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) {

168
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -71,25 +73,22 @@ public class EntityRelationController extends BaseController {
"If the user has the authority of 'Tenant Administrator', the server checks that the entity is owned by the same tenant. " + "If the user has the authority of 'Tenant Administrator', the server checks that the entity is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the entity is assigned to the same customer."; "If the user has the authority of 'Customer User', the server checks that the entity is assigned to the same customer.";
@ApiOperation(value = "Create Relation (saveRelation)", @Hidden
notes = "Creates or updates a relation between two entities in the platform. " +
"Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/relation") @PostMapping(value = "/relation")
public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) public void saveRelationV1(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException { @RequestBody EntityRelation relation) throws ThingsboardException {
doSave(relation); doSave(relation);
} }
@ApiOperation(value = "Create Relation (saveRelationV2)", @ApiOperation(value = "Create Relation (saveRelation)",
notes = "Creates or updates a relation between two entities in the platform. " + notes = "Creates or updates a relation between two entities in the platform. " +
"Relations unique key is a combination of from/to entity id and relation type group and relation type. " + "Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
SECURITY_CHECKS_ENTITIES_DESCRIPTION) SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/v2/relation") @PostMapping(value = "/v2/relation")
public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true) public EntityRelation saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException { @RequestBody EntityRelation relation) throws ThingsboardException {
return doSave(relation); return doSave(relation);
} }
@ -103,11 +102,10 @@ public class EntityRelationController extends BaseController {
return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
} }
@ApiOperation(value = "Delete Relation (deleteRelation)", @Hidden
notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) @DeleteMapping(value = "/relation")
public void deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, public void deleteRelationV1(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@ -116,16 +114,16 @@ public class EntityRelationController extends BaseController {
doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType); doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
} }
@ApiOperation(value = "Delete Relation (deleteRelationV2)", @ApiOperation(value = "Delete Relation (deleteRelation)",
notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/v2/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) @DeleteMapping(value = "/v2/relation")
public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, public EntityRelation deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
return doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType); return doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
} }
@ -144,11 +142,11 @@ public class EntityRelationController extends BaseController {
return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
} }
@ApiOperation(value = "Delete common relations (deleteCommonRelations)", @ApiOperation(value = "Delete common relations (deleteRelations)",
notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " + notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/relations", params = {"entityId", "entityType"}) @DeleteMapping(value = "/relations")
public void deleteRelations(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId, public void deleteRelations(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException {
checkParameter("entityId", strId); checkParameter("entityId", strId);
@ -161,7 +159,7 @@ public class EntityRelationController extends BaseController {
@ApiOperation(value = "Get Relation (getRelation)", @ApiOperation(value = "Get Relation (getRelation)",
notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) @GetMapping(value = "/relation")
public EntityRelation getRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, public EntityRelation getRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@ -180,14 +178,11 @@ public class EntityRelationController extends BaseController {
return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup)); return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup));
} }
@ApiOperation(value = "Get List of Relations (findByFrom)", @Hidden
notes = "Returns list of relation objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE}) @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE})
public List<EntityRelation> findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, public List<EntityRelation> findByFrom(@RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
checkParameter(FROM_ID, strFromId); checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType); checkParameter(FROM_TYPE, strFromType);
@ -197,9 +192,19 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup))); return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup)));
} }
@ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)", @ApiOperation(value = "Get List of Relations (findEntityRelationsByFrom)",
notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + notes = "Returns list of relation objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/from/{fromType}/{fromId}")
public List<EntityRelation> findEntityRelationsByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByFrom(strFromId, strFromType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/info", params = {FROM_ID, FROM_TYPE}) @GetMapping(value = "/relations/info", params = {FROM_ID, FROM_TYPE})
public List<EntityRelationInfo> findInfoByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, public List<EntityRelationInfo> findInfoByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@ -214,15 +219,24 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get())); return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get()));
} }
@ApiOperation(value = "Get List of Relations (findByFrom)", @ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByFrom)",
notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/info/from/{fromType}/{fromId}")
public List<EntityRelationInfo> findEntityRelationInfosByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException {
return findInfoByFrom(strFromId, strFromType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE, RELATION_TYPE})
public List<EntityRelation> findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, public List<EntityRelation> findByFrom(@RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @RequestParam(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
checkParameter(FROM_ID, strFromId); checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType); checkParameter(FROM_TYPE, strFromType);
@ -233,10 +247,21 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup))); return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup)));
} }
@ApiOperation(value = "Get List of Relations (findByTo)", @ApiOperation(value = "Get List of Relations (findEntityRelationsByFromAndRelationType)",
notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/from/{fromType}/{fromId}/{relationType}")
public List<EntityRelation> findEntityRelationsByFromAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByFrom(strFromId, strFromType, strRelationType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations", params = {TO_ID, TO_TYPE}) @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE})
public List<EntityRelation> findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, public List<EntityRelation> findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType,
@ -250,9 +275,19 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup))); return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup)));
} }
@ApiOperation(value = "Get List of Relation Infos (findInfoByTo)", @ApiOperation(value = "Get List of Relations (findEntityRelationsByTo)",
notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + notes = "Returns list of relation objects for the specified entity by the 'to' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/to/{toType}/{toId}")
public List<EntityRelation> findEntityRelationsByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByTo(strToId, strToType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/info", params = {TO_ID, TO_TYPE}) @GetMapping(value = "/relations/info", params = {TO_ID, TO_TYPE})
public List<EntityRelationInfo> findInfoByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, public List<EntityRelationInfo> findInfoByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@ -267,9 +302,19 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get())); return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get()));
} }
@ApiOperation(value = "Get List of Relations (findByTo)", @ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByTo)",
notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/info/to/{toType}/{toId}")
public List<EntityRelationInfo> findEntityRelationInfosByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException {
return findInfoByTo(strToId, strToType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations", params = {TO_ID, TO_TYPE, RELATION_TYPE}) @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE, RELATION_TYPE})
public List<EntityRelation> findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, public List<EntityRelation> findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@ -286,28 +331,41 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup))); return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup)));
} }
@ApiOperation(value = "Find related entities (findByQuery)", @ApiOperation(value = "Get List of Relations (findEntityRelationsByToAndRelationType)",
notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/relations/to/{toType}/{toId}/{relationType}")
public List<EntityRelation> findEntityRelationsByToAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByTo(strToId, strToType, strRelationType, strRelationTypeGroup);
}
@ApiOperation(value = "Find related entities (findEntityRelationsByQuery)",
notes = "Returns all entities that are related to the specific entity. " + notes = "Returns all entities that are related to the specific entity. " +
"The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " +
"See 'Model' tab of the Parameters for more info.") "See 'Model' tab of the Parameters for more info.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/relations") @PostMapping("/relations")
public List<EntityRelation> findByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) public List<EntityRelation> findEntityRelationsByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true)
@RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query.getParameters()); checkNotNull(query.getParameters());
checkNotNull(query.getFilters()); checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId(), Operation.READ); checkEntityId(query.getParameters().getEntityId(), Operation.READ);
return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get())); return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get()));
} }
@ApiOperation(value = "Find related entity infos (findInfoByQuery)", @ApiOperation(value = "Find related entity infos (findEntityRelationInfosByQuery)",
notes = "Returns all entity infos that are related to the specific entity. " + notes = "Returns all entity infos that are related to the specific entity. " +
"The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " +
"See 'Model' tab of the Parameters for more info. " + RELATION_INFO_DESCRIPTION) "See 'Model' tab of the Parameters for more info. " + RELATION_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/relations/info") @PostMapping("/relations/info")
public List<EntityRelationInfo> findInfoByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) public List<EntityRelationInfo> findEntityRelationInfosByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true)
@RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query.getParameters()); checkNotNull(query.getParameters());
checkNotNull(query.getFilters()); checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId(), Operation.READ); checkEntityId(query.getParameters().getEntityId(), Operation.READ);

46
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -23,7 +24,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -79,7 +79,6 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -87,6 +86,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -167,17 +167,25 @@ public class EntityViewController extends BaseController {
tbEntityViewService.delete(entityView, getCurrentUser()); tbEntityViewService.delete(entityView, getCurrentUser());
} }
@ApiOperation(value = "Get Entity View by name (getTenantEntityView)", @Hidden
notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityViews", params = {"entityViewName"}) @GetMapping(value = "/tenant/entityViews", params = {"entityViewName"})
public EntityView getTenantEntityView( public EntityView getTenantEntityView(
@Parameter(description = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException { @RequestParam String entityViewName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName)); return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName));
} }
@ApiOperation(value = "Get Entity View by name (getTenantEntityViewByName)",
notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityView")
public EntityView getTenantEntityViewByName(
@Parameter(description = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException {
return getTenantEntityView(entityViewName);
}
@ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)",
notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -222,7 +230,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of Entity View objects assigned to customer. " + notes = "Returns a page of Entity View objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}) @GetMapping(value = "/customer/{customerId}/entityViews")
public PageData<EntityView> getCustomerEntityViews( public PageData<EntityView> getCustomerEntityViews(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -254,7 +262,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION + notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}) @GetMapping(value = "/customer/{customerId}/entityViewInfos")
public PageData<EntityViewInfo> getCustomerEntityViewInfos( public PageData<EntityViewInfo> getCustomerEntityViewInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -286,7 +294,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION + notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}) @GetMapping(value = "/tenant/entityViews")
public PageData<EntityView> getTenantEntityViews( public PageData<EntityView> getTenantEntityViews(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -314,7 +322,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION + notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}) @GetMapping(value = "/tenant/entityViewInfos")
public PageData<EntityViewInfo> getTenantEntityViewInfos( public PageData<EntityViewInfo> getTenantEntityViewInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -337,13 +345,13 @@ public class EntityViewController extends BaseController {
} }
} }
@ApiOperation(value = "Find related entity views (findByQuery)", @ApiOperation(value = "Find related entity views (findEntityViewsByQuery)",
notes = "Returns all entity views that are related to the specific entity. " + notes = "Returns all entity views that are related to the specific entity. " +
"The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " + "The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/entityViews") @PostMapping(value = "/entityViews")
public List<EntityView> findByQuery( public List<EntityView> findEntityViewsByQuery(
@Parameter(description = "The entity view search query JSON") @Parameter(description = "The entity view search query JSON")
@RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { @RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query); checkNotNull(query);
@ -429,7 +437,7 @@ public class EntityViewController extends BaseController {
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}) @GetMapping(value = "/edge/{edgeId}/entityViews")
public PageData<EntityView> getEdgeEntityViews( public PageData<EntityView> getEdgeEntityViews(
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@RequestParam int pageSize, @RequestParam int pageSize,
@ -459,11 +467,10 @@ public class EntityViewController extends BaseController {
return checkNotNull(filteredResult); return checkNotNull(filteredResult);
} }
@ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)", @Hidden
notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/entityViews", params = {"entityViewIds"}) @GetMapping(value = "/entityViews", params = {"entityViewIds"})
public List<EntityView> getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) public List<EntityView> getEntityViewsByIdsV1(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("entityViewIds") Set<UUID> entityViewUUIDs) throws ThingsboardException { @RequestParam("entityViewIds") Set<UUID> entityViewUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<EntityViewId> entityViewIds = new ArrayList<>(); List<EntityViewId> entityViewIds = new ArrayList<>();
@ -474,6 +481,15 @@ public class EntityViewController extends BaseController {
return filterEntityViewsByReadPermission(entityViews); return filterEntityViewsByReadPermission(entityViews);
} }
@ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)",
notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/entityViews/list")
public List<EntityView> getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("entityViewIds") Set<UUID> entityViewUUIDs) throws ThingsboardException {
return getEntityViewsByIdsV1(entityViewUUIDs);
}
private List<EntityView> filterEntityViewsByReadPermission(List<EntityView> entityViews) { private List<EntityView> filterEntityViewsByReadPermission(List<EntityView> entityViews) {
return entityViews.stream().filter(entityView -> { return entityViews.stream().filter(entityView -> {
try { try {

19
application/src/main/java/org/thingsboard/server/controller/EventController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -113,13 +114,13 @@ public class EventController extends BaseController {
@Autowired @Autowired
private EventService eventService; private EventService eventService;
@ApiOperation(value = "Get Events by type (getEvents)", @ApiOperation(value = "Get Events by type (getEventsByType)",
notes = "Returns a page of events for specified entity by specifying event type. " + notes = "Returns a page of events for specified entity by specifying event type. " +
PAGE_DATA_PARAMETERS) PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET) @RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public PageData<EventInfo> getEvents( public PageData<EventInfo> getEventsByType(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -152,16 +153,12 @@ public class EventController extends BaseController {
return checkNotNull(eventService.findEvents(tenantId, entityId, resolveEventType(eventType), pageLink)); return checkNotNull(eventService.findEvents(tenantId, entityId, resolveEventType(eventType), pageLink));
} }
@ApiOperation(value = "Get Events (Deprecated)",
notes = "Returns a page of events for specified entity. Deprecated and will be removed in next minor release. " + @Hidden
"The call was deprecated to improve the performance of the system. " +
"Current implementation will return 'Lifecycle' events only. " +
"Use 'Get events by type' or 'Get events by filter' instead. " +
PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET) @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public PageData<EventInfo> getEvents( public PageData<EventInfo> getEventsDeprecated(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -194,14 +191,14 @@ public class EventController extends BaseController {
return checkNotNull(eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, pageLink)); return checkNotNull(eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, pageLink));
} }
@ApiOperation(value = "Get Events by event filter (getEvents)", @ApiOperation(value = "Get Events by event filter (getEventsByFilter)",
notes = "Returns a page of events for the chosen entity by specifying the event filter. " + notes = "Returns a page of events for the chosen entity by specifying the event filter. " +
PAGE_DATA_PARAMETERS + NEW_LINE + PAGE_DATA_PARAMETERS + NEW_LINE +
EVENT_FILTER_DEFINITION) EVENT_FILTER_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST) @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public PageData<EventInfo> getEvents( public PageData<EventInfo> getEventsByFilter(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)

4
application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java

@ -69,11 +69,11 @@ public class Lwm2mController extends BaseController {
return lwM2MService.getServerSecurityInfo(bootstrapServer); return lwM2MService.getServerSecurityInfo(bootstrapServer);
} }
@ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)") @ApiOperation(hidden = true, value = "Save LwM2M device with credentials (saveLwm2mDeviceWithCredentials) (Deprecated)")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST) @RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException { public Device saveLwm2mDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy()); return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy());

4
application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java

@ -43,12 +43,12 @@ public class MailConfigTemplateController extends BaseController {
private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports"; private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports";
private final TbMailConfigTemplateService mailConfigTemplateService; private final TbMailConfigTemplateService mailConfigTemplateService;
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getMailConfigTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
notes = MAIL_CONFIG_TEMPLATE_DEFINITION) notes = MAIL_CONFIG_TEMPLATE_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = "application/json") @RequestMapping(method = RequestMethod.GET, produces = "application/json")
@ResponseBody @ResponseBody
public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException { public JsonNode getMailConfigTemplates() throws ThingsboardException, IOException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return mailConfigTemplateService.findAllMailConfigTemplates(); return mailConfigTemplateService.findAllMailConfigTemplates();
} }

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

@ -81,12 +81,12 @@ public class MobileAppBundleController extends BaseController {
return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser()); return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser());
} }
@ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", @ApiOperation(value = "Update oauth2 clients (updateMobileAppBundleOauth2Clients)",
notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(value = "/mobile/bundle/{id}/oauth2Clients") @PutMapping(value = "/mobile/bundle/{id}/oauth2Clients")
public void updateOauth2Clients(@PathVariable UUID id, public void updateMobileAppBundleOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException { @RequestBody UUID[] clientIds) throws ThingsboardException {
MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id);
MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE); MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE);
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds); List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);

4
application/src/main/java/org/thingsboard/server/controller/MobileAppController.java

@ -123,7 +123,7 @@ public class MobileAppController extends BaseController {
return tbMobileAppService.save(mobileApp, getCurrentUser()); return tbMobileAppService.save(mobileApp, getCurrentUser());
} }
@ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Get mobile app infos (getTenantMobileApps)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/app") @GetMapping(value = "/mobile/app")
public PageData<MobileApp> getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS") public PageData<MobileApp> getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS")
@ -142,7 +142,7 @@ public class MobileAppController extends BaseController {
return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink); return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink);
} }
@ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Get mobile info by id (getMobileAppById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/app/{id}") @GetMapping(value = "/mobile/app/{id}")
public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException { public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException {

41
application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -153,12 +154,10 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink); return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink);
} }
@ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", @Hidden
notes = "Returns the list of notification targets found by provided ids." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets", params = {"ids"}) @GetMapping(value = "/targets", params = {"ids"})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) public List<NotificationTarget> getNotificationTargetsByIdsV1(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ids") UUID[] ids, @RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
// PE: generic permission // PE: generic permission
@ -166,6 +165,17 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds); return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds);
} }
@ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)",
notes = "Returns the list of notification targets found by provided ids." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets/list")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) {
return getNotificationTargetsByIdsV1(ids, user);
}
@ApiOperation(value = "Get notification targets (getNotificationTargets)", @ApiOperation(value = "Get notification targets (getNotificationTargets)",
notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE + notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE +
PAGE_DATA_PARAMETERS + PAGE_DATA_PARAMETERS +
@ -188,13 +198,10 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink);
} }
@ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", @Hidden
notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets", params = "notificationType") @GetMapping(value = "/targets", params = "notificationType")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationTypeV1(@RequestParam int pageSize,
@RequestParam int page, @RequestParam int page,
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ -206,6 +213,22 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink); return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink);
} }
@ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)",
notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets/notificationType/{notificationType}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@PathVariable NotificationType notificationType,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
return getNotificationTargetsBySupportedNotificationTypeV1(pageSize, page, textSearch, sortProperty, sortOrder, notificationType, user);
}
@ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", @ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)",
notes = "Deletes notification target by its id." + NEW_LINE + notes = "Deletes notification target by its id." + NEW_LINE +
"This target cannot be referenced by existing scheduled notification requests or any notification rules." + "This target cannot be referenced by existing scheduled notification requests or any notification rules." +

4
application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java

@ -71,12 +71,12 @@ public class OAuth2ConfigTemplateController extends BaseController {
oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId);
} }
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getOAuth2ClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION) notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = "application/json") @RequestMapping(method = RequestMethod.GET, produces = "application/json")
@ResponseBody @ResponseBody
public List<OAuth2ClientRegistrationTemplate> getClientRegistrationTemplates() throws ThingsboardException { public List<OAuth2ClientRegistrationTemplate> getOAuth2ClientRegistrationTemplates() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ);
return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates();
} }

15
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -132,15 +133,23 @@ public class OAuth2Controller extends BaseController {
return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink);
} }
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos", params = {"clientIds"})
public List<OAuth2ClientInfo> findTenantOAuth2ClientInfosByIdsV1(
@RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException {
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);
return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds);
}
@ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)",
notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) @GetMapping(value = "/oauth2/client/list")
public List<OAuth2ClientInfo> findTenantOAuth2ClientInfosByIds( public List<OAuth2ClientInfo> findTenantOAuth2ClientInfosByIds(
@Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException {
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds); return findTenantOAuth2ClientInfosByIdsV1(clientIds);
return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds);
} }
@ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)

30
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -181,25 +181,25 @@ public class OtaPackageController extends BaseController {
return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink)); return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink));
} }
@ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", @ApiOperation(value = "Get OTA Package Infos by device profile and type (getOtaPackagesByDeviceProfileAndType)",
notes = "Returns a page of OTA Package Info objects owned by tenant. " + notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/otaPackages/{deviceProfileId}/{type}") @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) public PageData<OtaPackageInfo> getOtaPackagesByDeviceProfileAndType(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId, @PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@PathVariable("type") String strType, @PathVariable("type") String strType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) @Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"})) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"}))
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("deviceProfileId", strDeviceProfileId); checkParameter("deviceProfileId", strDeviceProfileId);
checkParameter("type", strType); checkParameter("type", strType);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);

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

@ -129,7 +129,7 @@ public class QrCodeSettingsController extends BaseController {
return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings); return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings);
} }
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", @ApiOperation(value = "Get Mobile application settings (getQrCodeSettings)",
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/qr/settings") @GetMapping(value = "/api/mobile/qr/settings")

8
application/src/main/java/org/thingsboard/server/controller/QueueController.java

@ -19,7 +19,9 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -65,8 +67,7 @@ public class QueueController extends BaseController {
notes = "Returns a page of queues registered in the platform. " + notes = "Returns a page of queues registered in the platform. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/queues")
@ResponseBody
public PageData<Queue> getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) public PageData<Queue> getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam String serviceType, @RequestParam String serviceType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -122,8 +123,7 @@ public class QueueController extends BaseController {
"Remove 'id', 'tenantId' from the request body example (below) to create new Queue entity. " + "Remove 'id', 'tenantId' from the request body example (below) to create new Queue entity. " +
SYSTEM_AUTHORITY_PARAGRAPH) SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST) @PostMapping(value = "/queues")
@ResponseBody
public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.") public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.")
@RequestBody Queue queue, @RequestBody Queue queue,
@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED))

17
application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -85,12 +86,10 @@ public class QueueStatsController extends BaseController {
return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId)); return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId));
} }
@ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)", @Hidden
notes = "Fetch the Queue stats objects based on the provided ids. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats", params = {"queueStatsIds"}) @GetMapping(value = "/queueStats", params = {"queueStatsIds"})
public List<QueueStats> getQueueStatsByIds( public List<QueueStats> getQueueStatsByIdsV1(
@Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException {
checkArrayParameter("queueStatsIds", strQueueStatsIds); checkArrayParameter("queueStatsIds", strQueueStatsIds);
List<QueueStatsId> queueStatsIds = new ArrayList<>(); List<QueueStatsId> queueStatsIds = new ArrayList<>();
@ -99,4 +98,14 @@ public class QueueStatsController extends BaseController {
} }
return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds); return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds);
} }
@ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)",
notes = "Fetch the Queue stats objects based on the provided ids. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats/list")
public List<QueueStats> getQueueStatsByIds(
@Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException {
return getQueueStatsByIdsV1(strQueueStatsIds);
}
} }

8
application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java

@ -43,11 +43,11 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CU
@Slf4j @Slf4j
public class RpcV1Controller extends AbstractRpcController { public class RpcV1Controller extends AbstractRpcController {
@ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest( public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequestV1(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr, @PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.") @Parameter(description = "A JSON value representing the RPC request.")
@ -55,11 +55,11 @@ public class RpcV1Controller extends AbstractRpcController {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
} }
@ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest( public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequestV1(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr, @PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.") @Parameter(description = "A JSON value representing the RPC request.")

8
application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java

@ -113,7 +113,7 @@ public class RpcV2Controller extends AbstractRpcController {
private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@ApiOperation(value = "Send one-way RPC request", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION) @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV2)", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC request was sent to the device."), @ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC request was sent to the device."),
@ApiResponse(responseCode = "400", description = "Invalid structure of the request."), @ApiResponse(responseCode = "400", description = "Invalid structure of the request."),
@ -124,7 +124,7 @@ public class RpcV2Controller extends AbstractRpcController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest( public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequestV2(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr, @PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.") @Parameter(description = "A JSON value representing the RPC request.")
@ -132,7 +132,7 @@ public class RpcV2Controller extends AbstractRpcController {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
} }
@ApiOperation(value = "Send two-way RPC request", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION) @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV2)", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC response received."), @ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC response received."),
@ApiResponse(responseCode = "400", description = "Invalid structure of the request."), @ApiResponse(responseCode = "400", description = "Invalid structure of the request."),
@ -143,7 +143,7 @@ public class RpcV2Controller extends AbstractRpcController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest( public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequestV2(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String deviceIdStr, @PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.") @Parameter(description = "A JSON value representing the RPC request.")

32
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -229,12 +230,12 @@ public class RuleChainController extends BaseController {
return tbRuleChainService.save(ruleChain, getCurrentUser()); return tbRuleChainService.save(ruleChain, getCurrentUser());
} }
@ApiOperation(value = "Create Default Rule Chain", @ApiOperation(value = "Create Default Rule Chain (setDeviceDefaultRuleChain)",
notes = "Create rule chain from template, based on the specified name in the request. " + notes = "Create rule chain from template, based on the specified name in the request. " +
"Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/ruleChain/device/default") @PostMapping("/ruleChain/device/default")
public RuleChain saveRuleChain( public RuleChain setDeviceDefaultRuleChain(
@Parameter(description = "A JSON value representing the request.") @Parameter(description = "A JSON value representing the request.")
@RequestBody DefaultRuleChainCreateRequest request) throws Exception { @RequestBody DefaultRuleChainCreateRequest request) throws Exception {
checkNotNull(request); checkNotNull(request);
@ -281,7 +282,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chains (getRuleChains)", @ApiOperation(value = "Get Rule Chains (getRuleChains)",
notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains", params = {"pageSize", "page"}) @GetMapping(value = "/ruleChains")
public PageData<RuleChain> getRuleChains( public PageData<RuleChain> getRuleChains(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -347,7 +348,7 @@ public class RuleChainController extends BaseController {
notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/ruleChain/testScript") @PostMapping("/ruleChain/testScript")
public JsonNode testScript( public JsonNode testRuleChainScript(
@Parameter(description = "Script language: JS or TBEL") @Parameter(description = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang, @RequestParam(required = false) ScriptLanguage scriptLang,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.")
@ -409,7 +410,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains/export", params = {"limit"}) @GetMapping(value = "/ruleChains/export")
public RuleChainData exportRuleChains( public RuleChainData exportRuleChains(
@Parameter(description = "A limit of rule chains to export.", required = true) @Parameter(description = "A limit of rule chains to export.", required = true)
@RequestParam("limit") int limit) throws ThingsboardException { @RequestParam("limit") int limit) throws ThingsboardException {
@ -505,7 +506,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)",
notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}) @GetMapping(value = "/edge/{edgeId}/ruleChains")
public PageData<RuleChain> getEdgeRuleChains( public PageData<RuleChain> getEdgeRuleChains(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@ -582,14 +583,10 @@ public class RuleChainController extends BaseController {
return checkNotNull(result); return checkNotNull(result);
} }
@ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)", @Hidden
notes = "Requested rule chains must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains", params = {"ruleChainIds"}) @GetMapping(value = "/ruleChains", params = {"ruleChainIds"})
public List<RuleChain> getRuleChainsByIds( public List<RuleChain> getRuleChainsByIdsV1(@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
@Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChainId> ruleChainIds = new ArrayList<>(); List<RuleChainId> ruleChainIds = new ArrayList<>();
for (UUID ruleChainUUID : ruleChainUUIDs) { for (UUID ruleChainUUID : ruleChainUUIDs) {
@ -598,4 +595,15 @@ public class RuleChainController extends BaseController {
return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds); return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds);
} }
@ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)",
notes = "Requested rule chains must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains/list")
public List<RuleChain> getRuleChainsByIds(
@Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
return getRuleChainsByIdsV1(ruleChainUUIDs);
}
} }

26
application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java

@ -77,7 +77,7 @@ public class RuleEngineController extends BaseController {
@Autowired @Autowired
private AccessValidator accessValidator; private AccessValidator accessValidator;
@ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequestForUser)",
notes = MSG_DESCRIPTION_PREFIX + notes = MSG_DESCRIPTION_PREFIX +
"Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " + "Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " +
MSG_DESCRIPTION + MSG_DESCRIPTION +
@ -86,13 +86,13 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/", method = RequestMethod.POST) @RequestMapping(value = "/", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest( public DeferredResult<ResponseEntity> handleRuleEngineRequestForUser(
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(null, null, null, defaultResponseTimeout, requestBody); return handleRuleEngineRequestForEntityWithQueueAndTimeout(null, null, null, defaultResponseTimeout, requestBody);
} }
@ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequestForEntity)",
notes = MSG_DESCRIPTION_PREFIX + notes = MSG_DESCRIPTION_PREFIX +
"Uses specified Entity Id as the Rule Engine message originator. " + "Uses specified Entity Id as the Rule Engine message originator. " +
MSG_DESCRIPTION + MSG_DESCRIPTION +
@ -101,17 +101,17 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST) @RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest( public DeferredResult<ResponseEntity> handleRuleEngineRequestForEntity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable("entityType") String entityType, @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(entityType, entityIdStr, null, defaultResponseTimeout, requestBody); return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, defaultResponseTimeout, requestBody);
} }
@ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequestForEntityWithTimeout)",
notes = MSG_DESCRIPTION_PREFIX + notes = MSG_DESCRIPTION_PREFIX +
"Uses specified Entity Id as the Rule Engine message originator. " + "Uses specified Entity Id as the Rule Engine message originator. " +
MSG_DESCRIPTION + MSG_DESCRIPTION +
@ -120,7 +120,7 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST) @RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest( public DeferredResult<ResponseEntity> handleRuleEngineRequestForEntityWithTimeout(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable("entityType") String entityType, @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -129,10 +129,10 @@ public class RuleEngineController extends BaseController {
@PathVariable("timeout") int timeout, @PathVariable("timeout") int timeout,
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(entityType, entityIdStr, null, timeout, requestBody); return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, timeout, requestBody);
} }
@ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequestForEntityWithQueueAndTimeout)",
notes = MSG_DESCRIPTION_PREFIX + notes = MSG_DESCRIPTION_PREFIX +
"Uses specified Entity Id as the Rule Engine message originator. " + "Uses specified Entity Id as the Rule Engine message originator. " +
MSG_DESCRIPTION + MSG_DESCRIPTION +
@ -142,7 +142,7 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST) @RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest( public DeferredResult<ResponseEntity> handleRuleEngineRequestForEntityWithQueueAndTimeout(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable("entityType") String entityType, @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -244,5 +244,7 @@ public class RuleEngineController extends BaseController {
response != null ? response.getData() : ""); response != null ? response.getData() : "");
} }
private record LocalRequestMetaData(TbMsg request, SecurityUser user, DeferredResult<ResponseEntity> responseWriter) {} private record LocalRequestMetaData(TbMsg request, SecurityUser user,
DeferredResult<ResponseEntity> responseWriter) {
}
} }

19
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -118,7 +119,7 @@ public class TbResourceController extends BaseController {
.body(resource); .body(resource);
} }
@ApiOperation(value = "Download resource (downloadResource)", @ApiOperation(value = "Download resource (downloadResourceIfChanged)",
notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/resource/{resourceType}/{scope}/{key}") @GetMapping(value = "/resource/{resourceType}/{scope}/{key}")
@ -336,11 +337,10 @@ public class TbResourceController extends BaseController {
} }
} }
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") @Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource", params = {"resourceIds"}) @GetMapping(value = "/resource", params = {"resourceIds"})
public List<TbResourceInfo> getSystemOrTenantResourcesByIds( public List<TbResourceInfo> getSystemOrTenantResourcesByIdsV1(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException { @RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
List<TbResourceId> resourceIds = new ArrayList<>(); List<TbResourceId> resourceIds = new ArrayList<>();
@ -350,7 +350,16 @@ public class TbResourceController extends BaseController {
return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
} }
@ApiOperation(value = "Get All Resource Infos (getAllResources)", @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource/list")
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
return getSystemOrTenantResourcesByIdsV1(resourceUuids);
}
@ApiOperation(value = "Get All Resource Infos (getTenantResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " + notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")

95
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -24,7 +24,12 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ -166,7 +171,8 @@ public class TelemetryController extends BaseController {
"\n\n * SERVER_SCOPE - supported for all entity types;" + "\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" + "\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. " "\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string")))))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/keys/attributes") @GetMapping(value = "/{entityType}/{entityId}/keys/attributes")
public DeferredResult<ResponseEntity> getAttributeKeys( public DeferredResult<ResponseEntity> getAttributeKeys(
@ -180,7 +186,8 @@ public class TelemetryController extends BaseController {
"\n\n * SERVER_SCOPE - supported for all entity types;" + "\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" + "\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. " "\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string")))))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}") @GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}")
public DeferredResult<ResponseEntity> getAttributeKeysByScope( public DeferredResult<ResponseEntity> getAttributeKeysByScope(
@ -197,13 +204,18 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_DATA_EXAMPLE + ATTRIBUTE_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AttributeData.class)))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/attributes") @GetMapping(value = "/{entityType}/{entityId}/values/attributes")
public DeferredResult<ResponseEntity> getAttributes( public DeferredResult<ResponseEntity> getAttributes(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -220,7 +232,11 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_DATA_EXAMPLE + ATTRIBUTE_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = AttributeData.class)))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}") @GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}")
public DeferredResult<ResponseEntity> getAttributesByScope( public DeferredResult<ResponseEntity> getAttributesByScope(
@ -228,6 +244,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -237,7 +254,8 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Get time series keys (getTimeseriesKeys)", @ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
notes = "Returns a set of unique time series key names for the selected entity. " + notes = "Returns a set of unique time series key names for the selected entity. " +
"\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string")))))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/keys/timeseries") @GetMapping(value = "/{entityType}/{entityId}/keys/timeseries")
public DeferredResult<ResponseEntity> getTimeseriesKeys( public DeferredResult<ResponseEntity> getTimeseriesKeys(
@ -258,7 +276,11 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ LATEST_TS_STRICT_DATA_EXAMPLE + LATEST_TS_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries") @GetMapping(value = "/{entityType}/{entityId}/values/timeseries")
public DeferredResult<ResponseEntity> getLatestTimeseries( public DeferredResult<ResponseEntity> getLatestTimeseries(
@ -267,6 +289,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -274,7 +297,32 @@ public class TelemetryController extends BaseController {
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes));
} }
@ApiOperation(value = "Get time series data (getTimeseries)", @Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"})
public DeferredResult<ResponseEntity> getTimeseries(
@PathVariable("entityType") String entityType,
@PathVariable("entityId") String entityIdStr,
@RequestParam(name = "keys", required = false) String keysStr,
@RequestParam(name = "startTs") Long startTs,
@RequestParam(name = "endTs") Long endTs,
@RequestParam(name = "intervalType", required = false) IntervalType intervalType,
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@RequestParam(name = "timeZone", required = false) String timeZone,
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
}
@ApiOperation(value = "Get time series data (getTimeseriesHistory)",
notes = "Returns a range of time series values for specified entity. " + notes = "Returns a range of time series values for specified entity. " +
"Returns not aggregated data by default. " + "Returns not aggregated data by default. " +
"Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " + "Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " +
@ -282,10 +330,14 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ TS_STRICT_DATA_EXAMPLE + TS_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"keys", "startTs", "endTs"}) @GetMapping(value = "/{entityType}/{entityId}/values/timeseries/history")
public DeferredResult<ResponseEntity> getTimeseries( public DeferredResult<ResponseEntity> getTimeseriesHistory(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@ -310,13 +362,9 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); return getTimeseries(entityType, entityIdStr, keysStr, startTs, endTs, intervalType, interval, timeZone, limit, aggStr, orderBy, useStrictDataTypes, params);
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
} }
@ApiOperation(value = "Save device attributes (saveDeviceAttributes)", @ApiOperation(value = "Save device attributes (saveDeviceAttributes)",
@ -449,6 +497,9 @@ public class TelemetryController extends BaseController {
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." + " Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." +
" The replacement value will be fetched from the 'time series' table, and its timestamp will be the most recent one before the defined time-range. " + " The replacement value will be fetched from the 'time series' table, and its timestamp will be the most recent one before the defined time-range. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
@ -473,6 +524,7 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest, @RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest,
@Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") @Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted, @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
@ -530,6 +582,9 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)", @ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)",
notes = "Delete device attributes using provided Device Id, scope and a list of keys. " + notes = "Delete device attributes using provided Device Id, scope and a list of keys. " +
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ -544,6 +599,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
@ -552,9 +608,13 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)",
notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " + notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " +
"This operation is idempotent: keys that do not exist are silently ignored and the response is still 200 OK. " +
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Entity attributes were removed for the selected keys in the request (keys that did not exist are silently ignored). " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ -568,6 +628,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params); List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);

18
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -22,7 +23,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -133,7 +133,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenants", params = {"pageSize", "page"}) @GetMapping(value = "/tenants")
public PageData<Tenant> getTenants( public PageData<Tenant> getTenants(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -152,7 +152,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. "
+ TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) + TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenantInfos", params = {"pageSize", "page"}) @GetMapping(value = "/tenantInfos")
public PageData<TenantInfo> getTenantInfos( public PageData<TenantInfo> getTenantInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -169,9 +169,10 @@ public class TenantController extends BaseController {
return checkNotNull(tenantService.findTenantInfos(pageLink)); return checkNotNull(tenantService.findTenantInfos(pageLink));
} }
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/tenants", params = {"tenantIds"}) @GetMapping(value = "/tenants", params = {"tenantIds"})
public List<Tenant> getTenantsByIds( public List<Tenant> getTenantsByIdsV1(
@Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("tenantIds") Set<UUID> tenantUUIDs) throws ThingsboardException { @RequestParam("tenantIds") Set<UUID> tenantUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
@ -183,6 +184,15 @@ public class TenantController extends BaseController {
return filterTenantsByReadPermission(tenants); return filterTenantsByReadPermission(tenants);
} }
@ApiOperation(value = "Get Tenants list (getTenantsByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/tenants/list")
public List<Tenant> getTenantsByIds(
@Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("tenantIds") Set<UUID> tenantUUIDs) throws ThingsboardException {
return getTenantsByIdsV1(tenantUUIDs);
}
private List<Tenant> filterTenantsByReadPermission(List<Tenant> tenants) { private List<Tenant> filterTenantsByReadPermission(List<Tenant> tenants) {
return tenants.stream().filter(tenant -> { return tenants.stream().filter(tenant -> {
try { try {

15
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -242,7 +243,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. "
+ TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) + TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}) @GetMapping(value = "/tenantProfileInfos")
public PageData<EntityInfo> getTenantProfileInfos( public PageData<EntityInfo> getTenantProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -258,11 +259,19 @@ public class TenantProfileController extends BaseController {
return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink));
} }
@Hidden
@GetMapping(value = "/tenantProfiles", params = {"ids"}) @GetMapping(value = "/tenantProfiles", params = {"ids"})
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
public List<TenantProfile> getTenantProfilesByIds(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) public List<TenantProfile> getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) {
@RequestParam("ids") UUID[] ids) {
return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids);
} }
@ApiOperation(value = "Get Tenant Profile list (getTenantProfileList)")
@GetMapping(value = "/tenantProfiles/list")
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public List<TenantProfile> getTenantProfileList(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("ids") UUID[] ids) {
return getTenantProfilesByIds(ids);
}
} }

9
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java

@ -16,7 +16,11 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -187,7 +191,7 @@ public class TwoFactorAuthConfigController extends BaseController {
return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType); return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType);
} }
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderTypes)", notes =
"Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
"Example of response:\n" + "Example of response:\n" +
"```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" +
@ -195,7 +199,7 @@ public class TwoFactorAuthConfigController extends BaseController {
) )
@GetMapping("/providers") @GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException { public List<TwoFaProviderType> getAvailableTwoFaProviderTypes() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true) return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream() .map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()
.map(TwoFaProviderConfig::getProviderType) .map(TwoFaProviderConfig::getProviderType)
@ -261,6 +265,7 @@ public class TwoFactorAuthConfigController extends BaseController {
} }
@Data @Data
@Schema
public static class TwoFaAccountConfigUpdateRequest { public static class TwoFaAccountConfigUpdateRequest {
private boolean useByDefault; private boolean useByDefault;
} }

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

@ -99,7 +99,7 @@ public class TwoFactorAuthController extends BaseController {
} }
} }
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderInfos)", notes =
"Get the list of 2FA provider infos available for user to use. Example:\n" + "Get the list of 2FA provider infos available for user to use. Example:\n" +
"```\n[\n" + "```\n[\n" +
" {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" +
@ -108,7 +108,7 @@ public class TwoFactorAuthController extends BaseController {
"]\n```") "]\n```")
@GetMapping("/providers") @GetMapping("/providers")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException { public List<TwoFaProviderInfo> getAvailableTwoFaProviderInfos() throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true); Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true);
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user) return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user)

71
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -273,7 +274,7 @@ public class UserController extends BaseController {
notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." + notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users", params = {"pageSize", "page"}) @GetMapping(value = "/users")
public PageData<User> getUsers( public PageData<User> getUsers(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -334,7 +335,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Tenant Users (getTenantAdmins)", @ApiOperation(value = "Get Tenant Users (getTenantAdmins)",
notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}) @GetMapping(value = "/tenant/{tenantId}/users")
public PageData<User> getTenantAdmins( public PageData<User> getTenantAdmins(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId, @PathVariable(TENANT_ID) String strTenantId,
@ -357,7 +358,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Customer Users (getCustomerUsers)", @ApiOperation(value = "Get Customer Users (getCustomerUsers)",
notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}) @GetMapping(value = "/customer/{customerId}/users")
public PageData<User> getCustomerUsers( public PageData<User> getCustomerUsers(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -404,7 +405,7 @@ public class UserController extends BaseController {
"Search is been executed by email, firstName and lastName fields. " + "Search is been executed by email, firstName and lastName fields. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}) @GetMapping(value = "/users/assign/{alarmId}")
public PageData<UserEmailInfo> getUsersForAssign( public PageData<UserEmailInfo> getUsersForAssign(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("alarmId") String strAlarmId, @PathVariable("alarmId") String strAlarmId,
@ -456,10 +457,7 @@ public class UserController extends BaseController {
return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings();
} }
@ApiOperation(value = "Update user settings (saveUserSettings)", @Hidden
notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(value = "/user/settings") @PutMapping(value = "/user/settings")
public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException {
@ -467,8 +465,17 @@ public class UserController extends BaseController {
userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings); userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings);
} }
@ApiOperation(value = "Get user settings (getUserSettings)", @ApiOperation(value = "Update user settings (putGeneralUserSettings)",
notes = "Fetch the User settings based on authorized user. ") notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(value = "/user/settings/general")
public void putGeneralUserSettings(@RequestBody JsonNode settings) throws ThingsboardException {
putUserSettings(settings);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/settings") @GetMapping(value = "/user/settings")
public JsonNode getUserSettings() throws ThingsboardException { public JsonNode getUserSettings() throws ThingsboardException {
@ -478,20 +485,28 @@ public class UserController extends BaseController {
return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings();
} }
@ApiOperation(value = "Delete user settings (deleteUserSettings)", @ApiOperation(value = "Get user settings (getGeneralUserSettings)",
notes = "Fetch the User settings based on authorized user. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/settings/general")
public JsonNode getGeneralUserSettings() throws ThingsboardException {
return getUserSettings();
}
@ApiOperation(value = "Delete user settings (deleteGeneralUserSettings)",
notes = "Delete user settings by specifying list of json element xpaths. \n " + notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/user/settings/{paths}") @DeleteMapping(value = "/user/settings/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS) public void deleteGeneralUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths) throws ThingsboardException { @PathVariable(PATHS) String paths) throws ThingsboardException {
checkParameter(USER_ID, paths); checkParameter(USER_ID, paths);
SecurityUser currentUser = getCurrentUser(); SecurityUser currentUser = getCurrentUser();
userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(","))); userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(",")));
} }
@ApiOperation(value = "Update user settings (saveUserSettings)", @ApiOperation(value = "Update user settings (putUserSettings)",
notes = "Update user settings for authorized user. Only specified json elements will be updated." + notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@ -518,15 +533,15 @@ public class UserController extends BaseController {
return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings();
} }
@ApiOperation(value = "Delete user settings (deleteUserSettings)", @ApiOperation(value = "Delete user settings by type (deleteUserSettingsByType)",
notes = "Delete user settings by specifying list of json element xpaths. \n " + notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/user/settings/{type}/{paths}") @DeleteMapping(value = "/user/settings/{type}/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS) public void deleteUserSettingsByType(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths, @PathVariable(PATHS) String paths,
@Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") @Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".")
@PathVariable("type") String strType) throws ThingsboardException { @PathVariable("type") String strType) throws ThingsboardException {
checkParameter(USER_ID, paths); checkParameter(USER_ID, paths);
UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf);
checkNotReserved(strType, type); checkNotReserved(strType, type);
@ -534,7 +549,7 @@ public class UserController extends BaseController {
userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(","))); userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(",")));
} }
@ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", @ApiOperation(value = "Get information about last visited and starred dashboards (getUserDashboardsInfo)",
notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/dashboards") @GetMapping(value = "/user/dashboards")
@ -583,12 +598,10 @@ public class UserController extends BaseController {
userService.removeMobileSession(user.getTenantId(), mobileToken); userService.removeMobileSession(user.getTenantId(), mobileToken);
} }
@ApiOperation(value = "Get Users By Ids (getUsersByIds)", @Hidden
notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users", params = {"userIds"}) @GetMapping(value = "/users", params = {"userIds"})
public List<User> getUsersByIds( public List<User> getUsersByIdsV1(
@Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("userIds") Set<UUID> userUUIDs) throws ThingsboardException { @RequestParam("userIds") Set<UUID> userUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<UserId> userIds = new ArrayList<>(); List<UserId> userIds = new ArrayList<>();
@ -599,6 +612,16 @@ public class UserController extends BaseController {
return filterUsersByReadPermission(users); return filterUsersByReadPermission(users);
} }
@ApiOperation(value = "Get Users By Ids (getUsersByIds)",
notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users/list")
public List<User> getUsersByIds(
@Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("userIds") Set<UUID> userUUIDs) throws ThingsboardException {
return getUsersByIdsV1(userUUIDs);
}
private List<User> filterUsersByReadPermission(List<User> users) { private List<User> filterUsersByReadPermission(List<User> users) {
return users.stream().filter(user -> { return users.stream().filter(user -> {
try { try {

30
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -209,8 +210,7 @@ public class WidgetTypeController extends AutoCommitController {
} }
} }
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)", @Hidden
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}) @GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"})
@Deprecated @Deprecated
@ -229,19 +229,27 @@ public class WidgetTypeController extends AutoCommitController {
return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId()));
} }
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", @Hidden
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"}) @GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"})
public List<WidgetType> getBundleWidgetTypes( public List<WidgetType> getBundleWidgetTypesV1(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId)); return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId));
} }
@ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)", @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)",
notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles/{widgetsBundleId}/widgetTypes")
public List<WidgetType> getBundleWidgetTypes(
@Parameter(description = "Widget Bundle Id", required = true)
@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
return getBundleWidgetTypesV1(strWidgetsBundleId);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}) @GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"})
@Deprecated @Deprecated
@ -284,7 +292,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)",
notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}) @GetMapping(value = "/widgetTypeFqns")
public List<String> getBundleWidgetTypeFqns( public List<String> getBundleWidgetTypeFqns(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@ -292,8 +300,7 @@ public class WidgetTypeController extends AutoCommitController {
return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId)); return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId));
} }
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", @Hidden
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}) @GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"})
@Deprecated @Deprecated
@ -344,8 +351,7 @@ public class WidgetTypeController extends AutoCommitController {
widgetTypeDeprecatedFilter, widgetTypes, pageLink)); widgetTypeDeprecatedFilter, widgetTypes, pageLink));
} }
@ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)", @Hidden
notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}) @GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"})
@Deprecated @Deprecated

31
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java

@ -15,13 +15,13 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -222,11 +222,10 @@ public class WidgetsBundleController extends BaseController {
} }
} }
@ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)", @Hidden
notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles") @GetMapping(value = "/widgetsBundles")
public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException { public List<WidgetsBundle> getWidgetsBundlesV1() throws ThingsboardException {
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId()));
} else { } else {
@ -235,13 +234,18 @@ public class WidgetsBundleController extends BaseController {
} }
} }
@ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesByIds)", @ApiOperation(value = "Get all Widget Bundles (getAllWidgetsBundles)",
notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " + notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
NEW_LINE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles/all")
public List<WidgetsBundle> getAllWidgetsBundles() throws ThingsboardException {
return getWidgetsBundlesV1();
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"}) @GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"})
public List<WidgetsBundle> getWidgetsBundlesByIds( public List<WidgetsBundle> getWidgetsBundlesByIds(
@Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("widgetsBundleIds") Set<UUID> widgetsBundleUUIDs) throws ThingsboardException { @RequestParam("widgetsBundleIds") Set<UUID> widgetsBundleUUIDs) throws ThingsboardException {
List<WidgetsBundleId> widgetsBundleIds = new ArrayList<>(); List<WidgetsBundleId> widgetsBundleIds = new ArrayList<>();
for (UUID widgetsBundleUUID : widgetsBundleUUIDs) { for (UUID widgetsBundleUUID : widgetsBundleUUIDs) {
@ -250,4 +254,15 @@ public class WidgetsBundleController extends BaseController {
return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds); return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds);
} }
@ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesList)",
notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles/list", params = {"widgetsBundleIds"})
public List<WidgetsBundle> getWidgetsBundlesList(
@Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("widgetsBundleIds") Set<UUID> widgetsBundleUUIDs) throws ThingsboardException {
return getWidgetsBundlesByIds(widgetsBundleUUIDs);
}
} }

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

12
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.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService; 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.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService; 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.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; 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.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.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -78,6 +80,7 @@ import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdge
import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor; import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor;
import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService; import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService;
import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; import org.thingsboard.server.service.executors.GrpcCallbackExecutorService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
@ -104,6 +107,9 @@ public class EdgeContextComponent {
} }
// services // services
@Autowired
private TelemetrySubscriptionService tsSubService;
@Autowired @Autowired
private AdminSettingsService adminSettingsService; private AdminSettingsService adminSettingsService;
@ -269,6 +275,12 @@ public class EdgeContextComponent {
@Autowired @Autowired
private AiModelProcessor aiModelProcessor; private AiModelProcessor aiModelProcessor;
@Autowired
private ApiKeyService apiKeyService;
@Autowired
private ApiKeyProcessor apiKeyProcessor;
@Autowired @Autowired
private UserProcessor userProcessor; private UserProcessor userProcessor;

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

@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel; 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.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
@ -60,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.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId; 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.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -97,6 +100,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; 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.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -193,6 +197,10 @@ public class EdgeMsgConstructorUtils {
) )
); );
private static void resetVersion(HasVersion entity) {
entity.setVersion(null);
}
public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) {
return AlarmUpdateMsg.newBuilder().setMsgType(msgType) return AlarmUpdateMsg.newBuilder().setMsgType(msgType)
.setEntity(JacksonUtil.toString(alarm)) .setEntity(JacksonUtil.toString(alarm))
@ -205,6 +213,7 @@ public class EdgeMsgConstructorUtils {
} }
public static AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) { public static AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) {
resetVersion(asset);
return AssetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(asset)) return AssetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(asset))
.setIdMSB(asset.getUuidId().getMostSignificantBits()) .setIdMSB(asset.getUuidId().getMostSignificantBits())
.setIdLSB(asset.getUuidId().getLeastSignificantBits()).build(); .setIdLSB(asset.getUuidId().getLeastSignificantBits()).build();
@ -218,6 +227,7 @@ public class EdgeMsgConstructorUtils {
} }
public static AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) { public static AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) {
resetVersion(assetProfile);
return AssetProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(assetProfile)) return AssetProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(assetProfile))
.setIdMSB(assetProfile.getId().getId().getMostSignificantBits()) .setIdMSB(assetProfile.getId().getId().getMostSignificantBits())
.setIdLSB(assetProfile.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(assetProfile.getId().getId().getLeastSignificantBits()).build();
@ -231,6 +241,7 @@ public class EdgeMsgConstructorUtils {
} }
public static CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) { public static CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) {
resetVersion(customer);
return CustomerUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(customer)) return CustomerUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(customer))
.setIdMSB(customer.getId().getId().getMostSignificantBits()) .setIdMSB(customer.getId().getId().getMostSignificantBits())
.setIdLSB(customer.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(customer.getId().getId().getLeastSignificantBits()).build();
@ -244,6 +255,7 @@ public class EdgeMsgConstructorUtils {
} }
public static DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) { public static DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) {
resetVersion(dashboard);
return DashboardUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(dashboard)) return DashboardUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(dashboard))
.setIdMSB(dashboard.getId().getId().getMostSignificantBits()) .setIdMSB(dashboard.getId().getId().getMostSignificantBits())
.setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build();
@ -257,6 +269,7 @@ public class EdgeMsgConstructorUtils {
} }
public static DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) { public static DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) {
resetVersion(device);
return DeviceUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(device)) return DeviceUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(device))
.setIdMSB(device.getId().getId().getMostSignificantBits()) .setIdMSB(device.getId().getId().getMostSignificantBits())
.setIdLSB(device.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(device.getId().getId().getLeastSignificantBits()).build();
@ -270,10 +283,12 @@ public class EdgeMsgConstructorUtils {
} }
public static DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) { public static DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) {
resetVersion(deviceCredentials);
return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build(); return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build();
} }
public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile, EdgeVersion edgeVersion) { public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
resetVersion(deviceProfile);
String entity = getEntityAndFixLwm2mBootstrapShortServerId(deviceProfile, edgeVersion); String entity = getEntityAndFixLwm2mBootstrapShortServerId(deviceProfile, edgeVersion);
return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(entity) return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(entity)
.setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits())
@ -387,6 +402,7 @@ public class EdgeMsgConstructorUtils {
} }
public static EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView) { public static EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView) {
resetVersion(entityView);
return EntityViewUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityView)) return EntityViewUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityView))
.setIdMSB(entityView.getId().getId().getMostSignificantBits()) .setIdMSB(entityView.getId().getId().getMostSignificantBits())
.setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build();
@ -486,6 +502,7 @@ public class EdgeMsgConstructorUtils {
} }
public static RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) { public static RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) {
resetVersion(entityRelation);
return RelationUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityRelation)).build(); return RelationUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityRelation)).build();
} }
@ -503,6 +520,7 @@ public class EdgeMsgConstructorUtils {
} }
public static RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) { public static RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) {
resetVersion(ruleChain);
boolean isTemplateRoot = ruleChain.isRoot(); boolean isTemplateRoot = ruleChain.isRoot();
ruleChain.setRoot(isRoot); ruleChain.setRoot(isRoot);
RuleChainUpdateMsg result = RuleChainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChain)) RuleChainUpdateMsg result = RuleChainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChain))
@ -520,6 +538,7 @@ public class EdgeMsgConstructorUtils {
} }
public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) {
resetVersion(ruleChainMetaData);
String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion); String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion);
return RuleChainMetadataUpdateMsg.newBuilder() return RuleChainMetadataUpdateMsg.newBuilder()
@ -640,6 +659,7 @@ public class EdgeMsgConstructorUtils {
} }
public static TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) { public static TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) {
resetVersion(tenant);
return TenantUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenant)).build(); return TenantUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenant)).build();
} }
@ -648,6 +668,7 @@ public class EdgeMsgConstructorUtils {
} }
public static UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user) { public static UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user) {
resetVersion(user);
return UserUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(user)) return UserUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(user))
.setIdMSB(user.getId().getId().getMostSignificantBits()) .setIdMSB(user.getId().getId().getMostSignificantBits())
.setIdLSB(user.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(user.getId().getId().getLeastSignificantBits()).build();
@ -665,6 +686,7 @@ public class EdgeMsgConstructorUtils {
} }
public static WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List<String> widgets) { public static WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List<String> widgets) {
resetVersion(widgetsBundle);
return WidgetsBundleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetsBundle)) return WidgetsBundleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetsBundle))
.setWidgets(JacksonUtil.toString(widgets)) .setWidgets(JacksonUtil.toString(widgets))
.setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits())
@ -680,6 +702,7 @@ public class EdgeMsgConstructorUtils {
} }
public static WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) { public static WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) {
resetVersion(widgetTypeDetails);
return WidgetTypeUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetTypeDetails)) return WidgetTypeUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetTypeDetails))
.setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits()) .setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits())
.setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()).build();
@ -694,6 +717,7 @@ public class EdgeMsgConstructorUtils {
} }
public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) { public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) {
resetVersion(calculatedField);
return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField)) return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField))
.setIdMSB(calculatedField.getId().getId().getMostSignificantBits()) .setIdMSB(calculatedField.getId().getId().getMostSignificantBits())
.setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build();
@ -707,6 +731,7 @@ public class EdgeMsgConstructorUtils {
} }
public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) { public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
resetVersion(aiModel);
return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel)) return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
.setIdMSB(aiModel.getId().getId().getMostSignificantBits()) .setIdMSB(aiModel.getId().getId().getMostSignificantBits())
.setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build(); .setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
@ -719,6 +744,19 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build(); .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) { public static List<EdgeEvent> mergeAndFilterDownlinkDuplicates(List<EdgeEvent> edgeEvents) {
try { try {
edgeEvents = removeDownlinkDuplicates(edgeEvents); edgeEvents = removeDownlinkDuplicates(edgeEvents);

43
application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java

@ -0,0 +1,43 @@
/**
* 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 com.google.common.util.concurrent.FutureCallback;
import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
@Slf4j
@AllArgsConstructor
public class AttributeSaveCallback implements FutureCallback<Void> {
private final TenantId tenantId;
private final EdgeId edgeId;
private final String key;
private final Object value;
@Override
public void onSuccess(@Nullable Void result) {
log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t);
}
}

77
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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import io.grpc.Server; 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.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 io.grpc.stub.StreamObserver;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
@ -37,7 +40,8 @@ import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants; 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.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId; 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 org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@ -112,6 +115,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private String certFileResource; private String certFileResource;
@Value("${edges.rpc.ssl.private_key}") @Value("${edges.rpc.ssl.private_key}")
private String privateKeyResource; private String privateKeyResource;
@Value("${edges.rpc.ssl.key_password:}")
private String keyPassword;
@Value("${edges.state.persistToTelemetry:false}") @Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry; private boolean persistToTelemetry;
@Value("${edges.rpc.client_max_keep_alive_time_sec:1}") @Value("${edges.rpc.client_max_keep_alive_time_sec:1}")
@ -135,9 +140,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Lazy @Lazy
private EdgeContextComponent ctx; private EdgeContextComponent ctx;
@Autowired
private TelemetrySubscriptionService tsSubService;
@Autowired @Autowired
private TbClusterService clusterService; private TbClusterService clusterService;
@ -176,9 +178,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
.addService(this); .addService(this);
if (sslEnabled) { if (sslEnabled) {
try { try {
InputStream certFileIs = ResourceUtils.getInputStream(this, certFileResource); setupSsl(builder);
InputStream privateKeyFileIs = ResourceUtils.getInputStream(this, privateKeyResource);
builder.useTransportSecurity(certFileIs, privateKeyFileIs);
} catch (Exception e) { } catch (Exception e) {
log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e);
throw new RuntimeException("Unable to set up SSL context!", e); throw new RuntimeException("Unable to set up SSL context!", e);
@ -199,6 +199,33 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.info("Edge RPC service initialized!"); 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 @PreDestroy
public void destroy() { public void destroy() {
if (server != null) { if (server != null) {
@ -553,14 +580,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { private void save(TenantId tenantId, EdgeId edgeId, String key, long value) {
log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value);
if (persistToTelemetry) { if (persistToTelemetry) {
tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() ctx.getTsSubService().saveTimeseries(TimeseriesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(edgeId) .entityId(edgeId)
.entry(new LongDataEntry(key, value)) .entry(new LongDataEntry(key, value))
.callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) .callback(new AttributeSaveCallback(tenantId, edgeId, key, value))
.build()); .build());
} else { } else {
tsSubService.saveAttributes(AttributesSaveRequest.builder() ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(edgeId) .entityId(edgeId)
.scope(AttributeScope.SERVER_SCOPE) .scope(AttributeScope.SERVER_SCOPE)
@ -573,14 +600,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) { private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) {
log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value); log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value);
if (persistToTelemetry) { if (persistToTelemetry) {
tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() ctx.getTsSubService().saveTimeseries(TimeseriesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(edgeId) .entityId(edgeId)
.entry(new BooleanDataEntry(key, value)) .entry(new BooleanDataEntry(key, value))
.callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) .callback(new AttributeSaveCallback(tenantId, edgeId, key, value))
.build()); .build());
} else { } else {
tsSubService.saveAttributes(AttributesSaveRequest.builder() ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(edgeId) .entityId(edgeId)
.scope(AttributeScope.SERVER_SCOPE) .scope(AttributeScope.SERVER_SCOPE)
@ -590,32 +617,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private static class AttributeSaveCallback implements FutureCallback<Void> {
private final TenantId tenantId;
private final EdgeId edgeId;
private final String key;
private final Object value;
AttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) {
this.tenantId = tenantId;
this.edgeId = edgeId;
this.key = key;
this.value = value;
}
@Override
public void onSuccess(@Nullable Void result) {
log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t);
}
}
private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) { private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) {
try { try {
EdgeId edgeId = edge.getId(); EdgeId edgeId = edge.getId();

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

@ -25,6 +25,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.data.util.Pair; import org.springframework.data.util.Pair;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
@ -37,6 +38,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
@ -47,6 +49,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; 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.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -195,7 +198,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
startSyncProcess(fullSync); startSyncProcess(fullSync);
} else { } else {
syncInProgress = false; updateSyncInProgress(false);
} }
} }
if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) { if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) {
@ -241,6 +244,11 @@ public abstract class EdgeGrpcSession implements Closeable {
log.debug("[{}] onConfigurationUpdate [{}]", sessionId, edge); log.debug("[{}] onConfigurationUpdate [{}]", sessionId, edge);
this.tenantId = edge.getTenantId(); this.tenantId = edge.getTenantId();
this.edge = edge; this.edge = edge;
if (!this.edge.getCustomerId().equals(edge.getCustomerId())) {
// do not send edge configuration message on customer update
// message send by separate flow from assign_to or unassing_from customer
return;
}
EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder()
.setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build(); .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build();
ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder()
@ -252,7 +260,7 @@ public abstract class EdgeGrpcSession implements Closeable {
public void startSyncProcess(boolean fullSync) { public void startSyncProcess(boolean fullSync) {
if (!syncInProgress) { if (!syncInProgress) {
log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId); log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId);
syncInProgress = true; updateSyncInProgress(true);
interruptGeneralProcessingOnSync(); interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync)); doSync(new EdgeSyncCursor(ctx, edge, fullSync));
} else { } else {
@ -398,6 +406,18 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getAttributesService().save(tenantId, edge.getId(), AttributeScope.SERVER_SCOPE, attributeKvEntry); ctx.getAttributesService().save(tenantId, edge.getId(), AttributeScope.SERVER_SCOPE, attributeKvEntry);
} }
private void updateSyncInProgress(Boolean value) {
this.syncInProgress = value;
ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(edge.getId())
.scope(AttributeScope.SERVER_SCOPE)
.entry(new BooleanDataEntry(DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value))
.callback(new AttributeSaveCallback(tenantId, edge.getId(), DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value))
.build());
}
private void interruptGeneralProcessingOnSync() { private void interruptGeneralProcessingOnSync() {
log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", tenantId, edge.getId(), sessionId); log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", tenantId, edge.getId(), sessionId);
stopCurrentSendDownlinkMsgsTask(true); stopCurrentSendDownlinkMsgsTask(true);
@ -766,7 +786,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
private void markSyncCompletedSendEdgeEventUpdate() { private void markSyncCompletedSendEdgeEventUpdate() {
syncInProgress = false; updateSyncInProgress(false);
ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId())); ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId()));
} }
@ -969,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) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); 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); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -19,6 +19,7 @@ import lombok.Getter;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AiModelEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AiModelEdgeEventFetcher;
@ -63,7 +64,8 @@ public class EdgeSyncCursor {
fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService())); fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService()));
fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService()));
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), TenantId.SYS_TENANT_ID));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), edge.getTenantId()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService()));

34
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java

@ -17,50 +17,32 @@ package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.ArrayList;
import java.util.List;
@AllArgsConstructor @AllArgsConstructor
@Slf4j @Slf4j
public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { public class AdminSettingsEdgeEventFetcher extends BasePageableEdgeEventFetcher<AdminSettings> {
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final TenantId fetcherTenantId;
@Override @Override
public PageLink getPageLink(int pageSize) { PageData<AdminSettings> fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) {
return null; return adminSettingsService.findAllByTenantId(fetcherTenantId, pageLink);
}
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = fetchAdminSettingsForKeys(tenantId, edge.getId(), List.of("general", "mail", "connectivity", "jwt"));
// return PageData object to be in sync with other fetchers
return new PageData<>(result, 1, result.size(), false);
} }
private List<EdgeEvent> fetchAdminSettingsForKeys(TenantId tenantId, EdgeId edgeId, List<String> keys) { @Override
List<EdgeEvent> result = new ArrayList<>(); EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AdminSettings adminSettings) {
for (String key : keys) { return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key); EdgeEventActionType.UPDATED, adminSettings.getId(), null);
if (adminSettings != null) {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(adminSettings)));
}
}
return result;
} }
} }

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java

@ -41,7 +41,7 @@ public class OAuth2EdgeEventFetcher extends BasePageableEdgeEventFetcher<DomainI
@Override @Override
EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, DomainInfo domainInfo) { EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, DomainInfo domainInfo) {
return EdgeUtils.constructEdgeEvent(TenantId.SYS_TENANT_ID, edge.getId(), EdgeEventType.DOMAIN, return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.DOMAIN,
EdgeEventActionType.ADDED, domainInfo.getId(), null); EdgeEventActionType.ADDED, domainInfo.getId(), null);
} }

5
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; UPDATED_COMMENT, DELETED -> true;
default -> switch (type) { default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, 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; NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false; default -> false;
}; };
@ -412,6 +412,9 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
} }
protected boolean isSaveRequired(HasVersion current, HasVersion updated) { protected boolean isSaveRequired(HasVersion current, HasVersion updated) {
if (current != null) {
current.setVersion(null);
}
updated.setVersion(null); updated.setVersion(null);
return !updated.equals(current); return !updated.equals(current);
} }

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);
}
}
}

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AdminSettingsId;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@ -35,7 +36,14 @@ public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor {
@Override @Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class); AdminSettings adminSettings = null;
if (edgeEvent.getEntityId() != null) {
AdminSettingsId adminSettingsId = new AdminSettingsId(edgeEvent.getEntityId());
adminSettings = edgeCtx.getAdminSettingsService().findAdminSettingsById(edgeEvent.getTenantId(), adminSettingsId);
} else if (edgeEvent.getBody() != null && !edgeEvent.getBody().isEmpty()) {
// legacy
adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class);
}
if (adminSettings == null) { if (adminSettings == null) {
return null; return null;
} }

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.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.User; 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.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; 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.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.exception.DataValidationException; 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.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; 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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Slf4j @Slf4j
@ -135,6 +138,10 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso
if (userCredentialsByUserId != null) { if (userCredentialsByUserId != null) {
builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); 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(); return builder.build();
} }
} }

7
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -360,10 +361,12 @@ public class EntityStateSourcingListener {
jobManager.onJobUpdate(job); jobManager.onJobUpdate(job);
ComponentLifecycleEvent event; ComponentLifecycleEvent event;
if (job.getResult().getCancellationTs() > 0) { if (job.getStatus() == JobStatus.CANCELLED) {
event = ComponentLifecycleEvent.STOPPED; event = ComponentLifecycleEvent.STOPPED;
} else if (job.getResult().getGeneralError() != null) { } else if (job.getStatus() == JobStatus.FAILED) {
event = ComponentLifecycleEvent.FAILED; event = ComponentLifecycleEvent.FAILED;
} else if (job.getStatus() == JobStatus.COMPLETED) {
event = ComponentLifecycleEvent.UPDATED;
} else { } else {
return; return;
} }

3
application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java

@ -31,7 +31,8 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// map of versions from which the upgrade to the current version is possible // map of versions from which the upgrade to the current version is possible
// key - supported version prefix, value - display name // key - supported version prefix, value - display name
private static final Map<String, String> SUPPORTED_VERSIONS_FOR_UPGRADE = Map.of( private static final Map<String, String> SUPPORTED_VERSIONS_FOR_UPGRADE = Map.of(
"4.3.0", "4.3.0.x" "4.3.0", "4.3.0.x",
"4.3.1", "4.3.1.x"
); );
private final ProjectInfo projectInfo; private final ProjectInfo projectInfo;

92
application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java

@ -15,15 +15,20 @@
*/ */
package org.thingsboard.server.service.job; package org.thingsboard.server.service.job;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -33,7 +38,10 @@ import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.Task;
import org.thingsboard.server.common.data.job.task.TaskResult; import org.thingsboard.server.common.data.job.task.TaskResult;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto;
@ -50,7 +58,10 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -65,6 +76,8 @@ public class DefaultJobManager implements JobManager {
private final Map<JobType, JobProcessor> jobProcessors; private final Map<JobType, JobProcessor> jobProcessors;
private final Map<JobType, TbQueueProducer<TbProtoQueueMsg<TaskProto>>> taskProducers; private final Map<JobType, TbQueueProducer<TbProtoQueueMsg<TaskProto>>> taskProducers;
private final ExecutorService executor; private final ExecutorService executor;
private final ConcurrentHashMap<JobId, TbCallback> finishCallbacks = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupScheduler;
public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService, public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService,
TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig,
@ -76,6 +89,8 @@ public class DefaultJobManager implements JobManager {
this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity()));
this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer));
this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass());
this.cleanupScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("job-callback-cleanup");
this.cleanupScheduler.scheduleWithFixedDelay(this::cleanupStaleCallbacks, 1, 1, TimeUnit.HOURS);
} }
@Override @Override
@ -84,6 +99,25 @@ public class DefaultJobManager implements JobManager {
return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor); return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor);
} }
@Override
public ListenableFuture<Job> submitJob(Job job, TbCallback finishCallback) {
ListenableFuture<Job> saveFuture = submitJob(job);
if (finishCallback != null) {
Futures.addCallback(saveFuture, new FutureCallback<>() {
@Override
public void onSuccess(Job savedJob) {
finishCallbacks.put(savedJob.getId(), finishCallback);
}
@Override
public void onFailure(Throwable t) {
finishCallback.onFailure(t);
}
}, MoreExecutors.directExecutor());
}
return saveFuture;
}
@Override @Override
public void onJobUpdate(Job job) { public void onJobUpdate(Job job) {
JobStatus status = job.getStatus(); JobStatus status = job.getStatus();
@ -109,6 +143,35 @@ public class DefaultJobManager implements JobManager {
} }
} }
@EventListener
public void onJobUpdateEvent(ComponentLifecycleMsg event) {
EntityId entityId = event.getEntityId();
if (entityId.getEntityType() != EntityType.JOB) {
return;
}
ComponentLifecycleEvent lifecycleEvent = event.getEvent();
if (!lifecycleEvent.equals(ComponentLifecycleEvent.STOPPED) &&
!lifecycleEvent.equals(ComponentLifecycleEvent.FAILED) &&
!lifecycleEvent.equals(ComponentLifecycleEvent.UPDATED)) {
return;
}
JobId jobId = new JobId(entityId.getId());
TbCallback callback = finishCallbacks.remove(jobId);
if (callback == null) {
return;
}
executor.execute(() -> {
try {
Job job = jobService.findJobById(event.getTenantId(), jobId);
invokeFinishCallback(job, callback);
} catch (Throwable e) {
log.error("[{}] Failed to invoke finish callback", jobId, e);
callback.onFailure(e);
}
});
}
private void processJob(Job job) { private void processJob(Job job) {
TenantId tenantId = job.getTenantId(); TenantId tenantId = job.getTenantId();
JobId jobId = job.getId(); JobId jobId = job.getId();
@ -195,12 +258,41 @@ public class DefaultJobManager implements JobManager {
}); });
} }
private void invokeFinishCallback(@Nullable Job job, TbCallback callback) {
if (job == null) {
callback.onFailure(new RuntimeException("Job not found"));
} else if (job.getStatus() == JobStatus.COMPLETED) {
callback.onSuccess();
} else {
callback.onFailure(new RuntimeException(job.getError()));
}
}
private void cleanupStaleCallbacks() {
finishCallbacks.entrySet().removeIf(entry -> {
JobId jobId = entry.getKey();
try {
Job job = jobService.findJobById(TenantId.SYS_TENANT_ID, jobId);
if (job == null || job.getStatus().isOneOf(JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED)) {
invokeFinishCallback(job, entry.getValue());
return true;
}
return false;
} catch (Throwable e) {
log.error("[{}] Failed to cleanup stale callback", jobId, e);
entry.getValue().onFailure(e);
return true;
}
});
}
private JobProcessor getJobProcessor(JobType jobType) { private JobProcessor getJobProcessor(JobType jobType) {
return jobProcessors.get(jobType); return jobProcessors.get(jobType);
} }
@PreDestroy @PreDestroy
private void destroy() { private void destroy() {
cleanupScheduler.shutdownNow();
executor.shutdownNow(); executor.shutdownNow();
} }

145
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -15,13 +15,15 @@
*/ */
package org.thingsboard.server.service.query; package org.thingsboard.server.service.query;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.KvUtil; import org.thingsboard.common.util.KvUtil;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -30,11 +32,17 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
@ -59,11 +67,13 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -253,7 +263,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
if (isAttributes) { if (isAttributes) {
Map<EntityType, List<EntityId>> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType)); Map<EntityType, List<EntityId>> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType));
List<ListenableFuture<List<String>>> futures = new ArrayList<>(typesMap.size()); List<ListenableFuture<List<String>>> futures = new ArrayList<>(typesMap.size());
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, scope)))); typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope))));
attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> { attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> {
if (CollectionUtils.isEmpty(lists)) { if (CollectionUtils.isEmpty(lists)) {
return Collections.emptyList(); return Collections.emptyList();
@ -274,4 +284,135 @@ public class DefaultEntityQueryService implements EntityQueryService {
}, dbCallbackExecutor); }, dbCallbackExecutor);
} }
@Override
public ListenableFuture<AvailableEntityKeysV2> findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query,
boolean includeTimeseries, boolean includeAttributes,
Set<AttributeScope> scopes, boolean includeSamples) {
if (!includeTimeseries && !includeAttributes) {
return Futures.immediateFailedFuture(
new IllegalArgumentException("At least one of 'includeTimeseries' or 'includeAttributes' must be true"));
}
return Futures.transformAsync(findEntityIdsByQueryAsync(securityUser, query), ids -> {
if (ids.isEmpty()) {
return immediateFuture(new AvailableEntityKeysV2(
Collections.emptySet(),
includeTimeseries ? Collections.emptyList() : null,
includeAttributes ? Collections.emptyMap() : null));
}
TenantId tenantId = securityUser.getTenantId();
Set<EntityType> entityTypes = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet());
var tsFuture = includeTimeseries ? fetchTimeseriesKeys(tenantId, ids, includeSamples) : null;
Set<AttributeScope> effectiveScopes = includeAttributes
? resolveAttributeScopes(scopes, entityTypes) : Collections.emptySet();
var attrFutures = effectiveScopes.stream()
.map(scope -> fetchAttributeKeys(tenantId, ids, scope, includeSamples))
.toList();
return assembleResult(entityTypes, tsFuture, attrFutures);
}, dbCallbackExecutor);
}
private ListenableFuture<List<EntityId>> findEntityIdsByQueryAsync(SecurityUser securityUser, EntityDataQuery query) {
return Futures.transform(entityService.findEntityDataByQueryAsync(securityUser.getTenantId(), securityUser.getCustomerId(), query),
page -> page.getData().stream()
.map(EntityData::getEntityId)
.toList(),
dbCallbackExecutor);
}
private static Set<AttributeScope> resolveAttributeScopes(Set<AttributeScope> requestedScopes, Set<EntityType> entityTypes) {
boolean hasDevices = entityTypes.contains(EntityType.DEVICE);
Set<AttributeScope> scopes;
if (CollectionUtils.isNotEmpty(requestedScopes)) {
scopes = requestedScopes;
} else { // auto-determine scopes
scopes = hasDevices
? Set.of(AttributeScope.SERVER_SCOPE, AttributeScope.CLIENT_SCOPE, AttributeScope.SHARED_SCOPE)
: Collections.singleton(AttributeScope.SERVER_SCOPE);
}
// Non-device entities only support SERVER_SCOPE
if (!hasDevices) {
return scopes.contains(AttributeScope.SERVER_SCOPE)
? Collections.singleton(AttributeScope.SERVER_SCOPE)
: Collections.emptySet();
}
return scopes;
}
private ListenableFuture<List<KeyInfo>> fetchTimeseriesKeys(TenantId tenantId, List<EntityId> entityIds, boolean includeSamples) {
if (includeSamples) {
return Futures.transform(
timeseriesService.findLatestByEntityIdsAsync(tenantId, entityIds),
entries -> toKeyInfos(entries, true),
dbCallbackExecutor);
}
return Futures.transform(
timeseriesService.findAllKeysByEntityIdsAsync(tenantId, entityIds),
keys -> keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList(),
dbCallbackExecutor);
}
private ListenableFuture<Map.Entry<AttributeScope, List<KeyInfo>>> fetchAttributeKeys(
TenantId tenantId, List<EntityId> entityIds, AttributeScope scope, boolean includeSamples) {
if (includeSamples) {
return Futures.transform(
attributesService.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope),
entries -> Map.entry(scope, toKeyInfos(entries, true)),
dbCallbackExecutor);
}
return Futures.transform(
attributesService.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope),
keys -> Map.entry(scope, keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList()),
dbCallbackExecutor);
}
private ListenableFuture<AvailableEntityKeysV2> assembleResult(
Set<EntityType> entityTypes,
ListenableFuture<List<KeyInfo>> tsFuture,
List<ListenableFuture<Map.Entry<AttributeScope, List<KeyInfo>>>> attrFutures) {
var allAttrFuture = attrFutures.isEmpty()
? immediateFuture(List.<Map.Entry<AttributeScope, List<KeyInfo>>>of())
: Futures.allAsList(attrFutures);
List<ListenableFuture<?>> allFutures = new ArrayList<>();
if (tsFuture != null) {
allFutures.add(tsFuture);
}
allFutures.add(allAttrFuture);
var finalTsFuture = tsFuture;
return Futures.whenAllComplete(allFutures)
.call(() -> {
List<KeyInfo> tsKeys = finalTsFuture != null ? Futures.getDone(finalTsFuture) : null;
Map<AttributeScope, List<KeyInfo>> attrMap = attrFutures.isEmpty() ? null : new TreeMap<>();
if (attrMap != null) {
for (var entry : Futures.getDone(allAttrFuture)) {
attrMap.put(entry.getKey(), entry.getValue());
}
}
return new AvailableEntityKeysV2(entityTypes, tsKeys, attrMap);
}, dbCallbackExecutor);
}
private static List<KeyInfo> toKeyInfos(List<? extends KvEntry> entries, boolean includeSamples) {
return entries.stream()
.map(e -> new KeyInfo(e.getKey(), includeSamples ? toKeySample(e) : null))
.sorted(Comparator.comparing(KeyInfo::key))
.toList();
}
private static KeySample toKeySample(KvEntry entry) {
long ts = entry instanceof TsKvEntry tsKv ? tsKv.getTs()
: entry instanceof AttributeKvEntry attr ? attr.getLastUpdateTs()
: 0;
JsonNode value = entry.getDataType() == DataType.JSON
? JacksonUtil.toJsonNode(entry.getJsonValue().get())
: JacksonUtil.valueToTree(entry.getValue());
return new KeySample(ts, value);
}
} }

7
application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java

@ -23,11 +23,14 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Set;
public interface EntityQueryService { public interface EntityQueryService {
long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query); long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query);
@ -41,4 +44,8 @@ public interface EntityQueryService {
ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope); boolean isTimeseries, boolean isAttributes, AttributeScope scope);
ListenableFuture<AvailableEntityKeysV2> findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query,
boolean includeTimeseries, boolean includeAttributes,
Set<AttributeScope> scopes, boolean includeSamples);
} }

10
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java

@ -79,9 +79,13 @@ public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen
} }
} }
if (user.has(EMAIL)) { if (user.has(EMAIL)) {
JsonNode email = user.get(EMAIL); JsonNode emailNode = user.get(EMAIL);
if (email != null && email.isTextual()) { if (emailNode != null && emailNode.isTextual()) {
updated.put(EMAIL, email.asText()); Object tokenEmail = attributes.get(EMAIL);
if (tokenEmail != null && !emailNode.asText().equals(tokenEmail.toString())) {
log.warn("Apple OAuth2 callback: ignoring email [{}] from user POST parameter " +
"that differs from validated ID token email [{}]", emailNode.asText(), tokenEmail);
}
} }
} }
} }

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); return new ApiKeyAuthenticationToken(securityUser);
} }
private SecurityUser authenticate(String key) { public SecurityUser authenticate(String key) {
if (StringUtils.isEmpty(key)) { if (StringUtils.isEmpty(key)) {
throw new BadCredentialsException("Empty API key"); throw new BadCredentialsException("Empty API key");
} }

10
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java

@ -29,18 +29,10 @@ public class RestAuthenticationDetails implements Serializable {
private final Client userAgent; private final Client userAgent;
public RestAuthenticationDetails(HttpServletRequest request) { public RestAuthenticationDetails(HttpServletRequest request) {
this.clientAddress = getClientIP(request); this.clientAddress = request.getRemoteAddr();
this.userAgent = getUserAgent(request); this.userAgent = getUserAgent(request);
} }
private static String getClientIP(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null) {
return request.getRemoteAddr();
}
return xfHeader.split(",")[0];
}
private static Client getUserAgent(HttpServletRequest request) { private static Client getUserAgent(HttpServletRequest request) {
Parser uaParser = new Parser(); Parser uaParser = new Parser();
return uaParser.parse(request.getHeader("User-Agent")); return uaParser.parse(request.getHeader("User-Agent"));

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.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.EntityId; 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.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
@ -242,7 +243,10 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
@Override @Override
public void onFailure(Throwable t) { 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); }, wsCallBackExecutor);
} }
@ -258,7 +262,18 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
handleLatestCmd(ctx, cmd.getLatestCmd()); handleLatestCmd(ctx, cmd.getLatestCmd());
} }
if (cmd.getTsCmd() != null) { 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 { } else {
checkAndSendInitialData(ctx); 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) { private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) {
if (!theCtx.isInitialDataSent()) { if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription()); 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()) { if (sub.isLatestValues()) {
for (TsKvEntry kv : data) { for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey()); Long stateTs = keyStates.get(kv.getKey());
if (stateTs == null || kv.getTs() >= stateTs) { if (stateTs == null || kv.getTs() >= stateTs || kv.isDeletedEntry()) {
if (updateData == null) { if (updateData == null) {
updateData = new ArrayList<>(); updateData = new ArrayList<>();
} }
@ -362,7 +362,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
for (TsKvEntry kv : data) { for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey()); Long stateTs = keyStates.get(kv.getKey());
if (stateTs != null) { if (stateTs != null) {
if (!sub.isLatestValues() || kv.getTs() >= stateTs) { if (!sub.isLatestValues() || kv.getTs() >= stateTs || kv.isDeletedEntry()) {
if (updateData == null) { if (updateData == null) {
updateData = new ArrayList<>(); updateData = new ArrayList<>();
} }

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(); 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.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; 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.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.dao.notification.NotificationRequestDao; import org.thingsboard.server.dao.notification.NotificationRequestDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import java.time.Instant;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME;
@Slf4j
@Service @Service
@ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0") @ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0")
@Slf4j
public class NotificationsCleanUpService extends AbstractCleanUpService { public class NotificationsCleanUpService extends AbstractCleanUpService {
private final SqlPartitioningRepository partitioningRepository; private final SqlPartitioningRepository partitioningRepository;
private final NotificationRequestDao notificationRequestDao; private final NotificationRequestDao notificationRequestDao;
private final TenantService tenantService;
@Value("${sql.ttl.notifications.ttl:2592000}") @Value("${sql.ttl.notifications.ttl:2592000}")
private long ttlInSec; private long ttlInSec;
@Value("${sql.notifications.partition_size:168}") @Value("${sql.notifications.partition_size:168}")
private int partitionSizeInHours; private int partitionSizeInHours;
@Value("${sql.ttl.notifications.removal_batch_size:10000}")
private int removalBatchSize;
public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository, public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository,
NotificationRequestDao notificationRequestDao) { NotificationRequestDao notificationRequestDao, TenantService tenantService) {
super(partitionService); super(partitionService);
this.partitioningRepository = partitioningRepository; this.partitioningRepository = partitioningRepository;
this.notificationRequestDao = notificationRequestDao; this.notificationRequestDao = notificationRequestDao;
this.tenantService = tenantService;
} }
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.notifications.checking_interval_ms:86400000})}", @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() { public void cleanUp() {
long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec); long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec);
long partitionDurationMs = TimeUnit.HOURS.toMillis(partitionSizeInHours); 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); partitioningRepository.cleanupPartitionsCache(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
return;
} }
long lastRemovedNotificationTs = partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs); long gap = TimeUnit.MINUTES.toMillis(10);
if (lastRemovedNotificationTs > 0) { long requestExpTime = expTime - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap;
long gap = TimeUnit.MINUTES.toMillis(10); cleanUpNotificationRequests(requestExpTime);
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); 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; package org.thingsboard.server.service.ttl.rpc;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 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.rpc.RpcDao;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent; 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.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@Slf4j @Slf4j
@RequiredArgsConstructor @Service
public class RpcCleanUpService { @TbCoreComponent
@Value("${sql.ttl.rpc.enabled}") @ConditionalOnExpression("${sql.ttl.rpc.enabled:true}")
private boolean ttlTaskExecutionEnabled; 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 TenantService tenantService;
private final PartitionService partitionService;
private final TbTenantProfileCache tenantProfileCache; 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}") @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.rpc.checking_interval})}", fixedDelayString = "${sql.ttl.rpc.checking_interval}")
public void cleanUp() { public void cleanUp() {
if (ttlTaskExecutionEnabled) { PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
PageLink tenantsBatchRequest = new PageLink(10_000, 0); for (TenantId tenantId : tenants) {
PageData<TenantId> tenantsIds; try {
do { if (!isTenantPartitionMine(tenantId)) {
tenantsIds = tenantService.findTenantsIds(tenantsBatchRequest); continue;
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));
}
} }
tenantsBatchRequest = tenantsBatchRequest.nextPageLink(); Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
} while (tenantsIds.hasNext()); 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 @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class AuthCmd implements WsCmd { public class AuthCmd implements WsCmd {
private int cmdId; private int cmdId;
private String token; private String token;
private String apiKey;
@Override @Override
public WsCmdType getType() { public WsCmdType getType() {
return WsCmdType.AUTH; return WsCmdType.AUTH;
} }
} }

51
application/src/main/resources/thingsboard-openapi.properties

@ -0,0 +1,51 @@
#
# 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.
#
# Lightweight startup for OpenAPI spec generation.
# Activated only via: --spring.profiles.active=openapi
spring.main.banner-mode=off
# Testcontainers PostgreSQL
database.ts.type=sql
database.ts_latest.type=sql
spring.datasource.url=jdbc:tc:postgresql:16.6:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.hikari.maximumPoolSize=5
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.order_by.default_null_ordering=last
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
# Disable transports
transport.http.enabled=false
transport.mqtt.enabled=false
transport.coap.enabled=false
transport.lwm2m.enabled=false
transport.snmp.enabled=false
coap.server.enabled=false
# Disable edges, integrations, EDQS
edges.enabled=false
integrations.rpc.enabled=false
service.integrations.supported=NONE
transport.gateway.dashboard.sync.enabled=false
queue.edqs.sync.enabled=false
queue.edqs.api.supported=false
usage.stats.report.enabled=false
service.type=monolith

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

@ -78,6 +78,10 @@ server:
max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}" max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}"
# Maximum number of alarms returned for single alarm subscription. For example, no more than 10,000 alarms on the alarm widget # Maximum number of alarms returned for single alarm subscription. For example, no more than 10,000 alarms on the alarm widget
max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}" max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}"
# Maximum size (bytes) of incoming WS text message
max_text_message_buffer_size: "${TB_SERVER_WS_MAX_TEXT_MESSAGE_BUFFER_SIZE:32768}"
# Maximum size (bytes) of incoming WS binary message
max_binary_message_buffer_size: "${TB_SERVER_WS_MAX_BINARY_MESSAGE_BUFFER_SIZE:32768}"
# Maximum queue size of the websocket updates per session. This restriction prevents infinite updates of WS # Maximum queue size of the websocket updates per session. This restriction prevents infinite updates of WS
max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}" max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}"
# Maximum time between WS session opening and sending auth command # Maximum time between WS session opening and sending auth command
@ -197,6 +201,8 @@ usage:
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
# Statistics reporting interval, set to send summarized data every 10 seconds by default # Statistics reporting interval, set to send summarized data every 10 seconds by default
interval: "${USAGE_STATS_REPORT_INTERVAL:60}" interval: "${USAGE_STATS_REPORT_INTERVAL:60}"
# Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates
urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}"
# Amount of statistic messages in pack # Amount of statistic messages in pack
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
check: check:
@ -339,6 +345,8 @@ cassandra:
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}" set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}"
# log one of cassandra queries with specified frequency (0 - logging is disabled) # log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
# Maximum total size in bytes of a Cassandra query result set across all pages. Default is 50MB. 0 means unlimited
max_result_set_size_in_bytes: "${CASSANDRA_QUERY_MAX_RESULT_SET_SIZE_IN_BYTES:52428800}"
tenant_rate_limits: tenant_rate_limits:
# Whether to print rate-limited tenant names when printing Cassandra query queue statistic # Whether to print rate-limited tenant names when printing Cassandra query queue statistic
print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}" print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}"
@ -424,10 +432,11 @@ sql:
edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month
alarms: alarms:
checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours 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: rpc:
enabled: "${SQL_TTL_RPC_ENABLED:true}" # Enable/disable TTL (Time To Live) for rpc call records 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 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: audit_logs:
enabled: "${SQL_TTL_AUDIT_LOGS_ENABLED:true}" # Enable/disable TTL (Time To Live) for audit log records 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 ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. The accuracy of the cleanup depends on the sql.audit_logs.partition_size
@ -436,6 +445,7 @@ sql:
enabled: "${SQL_TTL_NOTIFICATIONS_ENABLED:true}" # Enable/disable TTL (Time To Live) for notification center records 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 ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days
checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day 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: api_keys:
enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for expired api keys records 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 checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
@ -507,6 +517,12 @@ actors:
# Force acknowledgment of the incoming message for external rule nodes to decrease processing latency. # Force acknowledgment of the incoming message for external rule nodes to decrease processing latency.
# Enqueue the result of external node processing as a separate message to the rule engine. # Enqueue the result of external node processing as a separate message to the rule engine.
force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}" force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}"
# Enable Server-Side Request Forgery (SSRF) protection for external HTTP rule nodes (rest api call).
# When enabled, requests to private/internal network addresses are blocked.
ssrf_protection_enabled: "${SSRF_PROTECTION_ENABLED:false}"
# 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:}"
rpc: rpc:
# Maximum number of persistent RPC call retries in case of failed request delivery. # Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}" max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
@ -1493,10 +1509,17 @@ edges:
ssl: ssl:
# Enable/disable SSL support # Enable/disable SSL support
enabled: "${EDGES_RPC_SSL_ENABLED:false}" 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}" 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}" 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 # 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}" 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. # Maximum length of telemetry (time-series and attributes) message the cloud sends to the edge. By default, there is no limitation.
@ -2039,7 +2062,7 @@ management:
web: web:
exposure: exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}' include: "${METRICS_ENDPOINTS_EXPOSE:info}"
health: health:
elasticsearch: elasticsearch:
# Enable the org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator.doHealthCheck # Enable the org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator.doHealthCheck

43
application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem;
@ -134,6 +135,48 @@ class DefaultTbContextTest {
then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any());
} }
@Test
public void givenMsgWithCurrentNodeAlreadyInStack_whenInput_thenTellFailureToPreventLoop() {
// GIVEN - simulate the second iteration: current node is already in the return stack,
// which happens when forwardMsgToDefaultRuleChain=true and the device's default rule chain
// is the same as the rule chain containing this node
var callbackMock = mock(TbMsgCallback.class);
given(callbackMock.isMsgValid()).willReturn(true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.off());
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(TENANT_ID)
.copyMetaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_STRING)
.callback(callbackMock)
.build();
// Push current node into the stack - simulates a previous iteration that already forwarded to this rule chain
msg.pushToStack(RULE_CHAIN_ID, RULE_NODE_ID);
var targetRuleChainId = new RuleChainId(UUID.randomUUID());
// WHEN
defaultTbContext.input(msg, targetRuleChainId);
// THEN - loop detected: tellFailure is called and no message is enqueued
ArgumentCaptor<TbActorMsg> tellCaptor = ArgumentCaptor.forClass(TbActorMsg.class);
then(chainActorMock).should().tell(tellCaptor.capture());
TbActorMsg capturedTellMsg = tellCaptor.getValue();
assertThat(capturedTellMsg).isInstanceOf(RuleNodeToRuleChainTellNextMsg.class);
RuleNodeToRuleChainTellNextMsg failureMsg = (RuleNodeToRuleChainTellNextMsg) capturedTellMsg;
assertThat(failureMsg.getRelationTypes()).containsOnly(TbNodeConnectionType.FAILURE);
assertThat(failureMsg.getFailureMessage()).containsIgnoringCase("loop");
then(mainCtxMock).shouldHaveNoInteractions(); // no message was enqueued
}
@MethodSource @MethodSource
@ParameterizedTest @ParameterizedTest
public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) {

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

@ -86,12 +86,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
} }
@Before @Before
public void beforeWsTest() throws Exception { public void beforeWsTest() {
// placeholder // placeholder
} }
@After @After
public void afterWsTest() throws Exception { public void afterWsTest() {
if (wsClient != null) { if (wsClient != null) {
wsClient.close(); wsClient.close();
} }
@ -113,4 +113,11 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
return wsClient; 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;
}
} }

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

@ -123,6 +123,8 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
@ -177,6 +179,7 @@ import java.nio.charset.StandardCharsets;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -422,6 +425,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
public void teardownWebTest() throws Exception { public void teardownWebTest() throws Exception {
log.debug("Executing web test teardown"); log.debug("Executing web test teardown");
// Drain any pending housekeeper work left by the test body (e.g., bulk tenant deletes)
// before proceeding with teardown deletions, to avoid 90s per-tenant wait timing out.
awaitHousekeeperDrained();
loginSysAdmin(); loginSysAdmin();
deleteTenant(tenantId); deleteTenant(tenantId);
deleteDifferentTenant(); deleteDifferentTenant();
@ -430,6 +437,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID);
jdbcTemplate.execute("TRUNCATE TABLE notification"); jdbcTemplate.execute("TRUNCATE TABLE notification");
jdbcTemplate.execute("TRUNCATE TABLE audit_log");
log.debug("Executed web test teardown"); log.debug("Executed web test teardown");
} }
@ -453,6 +461,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
.until(() -> storage.getLag("tb_housekeeper") == 0); .until(() -> storage.getLag("tb_housekeeper") == 0);
} }
protected void awaitHousekeeperDrained() {
Awaitility.await("housekeeper drained").atMost(5, TimeUnit.MINUTES).during(300, TimeUnit.MILLISECONDS)
.until(() -> storage.getLag("tb_housekeeper") == 0);
}
private List<Tenant> getAllTenants() throws Exception { private List<Tenant> getAllTenants() throws Exception {
List<Tenant> loadedTenants = new ArrayList<>(); List<Tenant> loadedTenants = new ArrayList<>();
PageLink pageLink = new PageLink(10); PageLink pageLink = new PageLink(10);
@ -717,6 +730,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return assetProfile; return assetProfile;
} }
protected Device createDevice(String name) throws Exception {
return createDevice(name, "default", null, null);
}
protected Device createDevice(String name, String accessToken) throws Exception { protected Device createDevice(String name, String accessToken) throws Exception {
return createDevice(name, "default", null, accessToken); return createDevice(name, "default", null, accessToken);
} }
@ -730,7 +747,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
deviceData.setConfiguration(new DefaultDeviceConfiguration()); deviceData.setConfiguration(new DefaultDeviceConfiguration());
device.setDeviceData(deviceData); device.setDeviceData(deviceData);
return doPost("/api/device?accessToken=" + accessToken, device, Device.class); if (accessToken != null) {
return doPost("/api/device?accessToken=" + accessToken, device, Device.class);
} else {
return doPost("/api/device", device, Device.class);
}
} }
protected Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId) { protected Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId) {
@ -1218,7 +1239,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId); CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() < boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() <
System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady; return isReady;
}); });
@ -1410,7 +1431,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception { protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception {
return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) +
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData();
} }
@ -1424,12 +1445,37 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected void postTelemetry(EntityId entityId, String payload) throws Exception { protected void postTelemetry(EntityId entityId, String payload) throws Exception {
doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
}
protected void postTelemetry(EntityId entityId, TsKvEntry entry) throws Exception {
var values = JacksonUtil.newObjectNode();
JacksonUtil.addKvEntry(values, entry);
var payload = JacksonUtil.newObjectNode()
.put("ts", entry.getTs())
.set("values", values);
var url = "/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/timeseries/any";
doPostAsync(url, payload, 30_000L).andExpect(status().isOk());
} }
protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception {
doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
}
protected void postAttributes(EntityId entityId, AttributeScope scope, KvEntry... attributes) throws Exception {
postAttributes(entityId, scope, Arrays.asList(attributes));
}
protected void postAttributes(EntityId entityId, AttributeScope scope, Collection<? extends KvEntry> attributes) throws Exception {
var url = "/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/attributes/" + scope;
var payload = JacksonUtil.newObjectNode();
for (KvEntry entry : attributes) {
JacksonUtil.addKvEntry(payload, entry);
}
doPostAsync(url, payload, 30_000L).andExpect(status().isOk());
} }
protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { protected CalculatedField saveCalculatedField(CalculatedField calculatedField) {
@ -1438,7 +1484,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" +
(type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink);
} }
protected PageData<String> getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { protected PageData<String> getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception {
@ -1451,11 +1497,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
List<UUID> entities, List<UUID> entities,
List<String> names) throws Exception { List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?" + return doGetTypedWithPageLink("/api/calculatedFields?" +
(type != null ? "types=" + type + "&" : "") + (type != null ? "types=" + type + "&" : "") +
(entityType != null ? "entityType=" + entityType + "&" : "") + (entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",", (entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") + entities.stream().map(UUID::toString).toList()) + "&" : "") +
(names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""),
new TypeReference<PageData<CalculatedFieldInfo>>() {}, new PageLink(10)).getData(); new TypeReference<PageData<CalculatedFieldInfo>>() {}, new PageLink(10)).getData();
} }

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();
}
}
}

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

@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -61,6 +62,9 @@ import static org.mockito.Mockito.verify;
@DaoSqlTest @DaoSqlTest
public class AuditLogControllerTest extends AbstractControllerTest { 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 Tenant savedTenant;
private User tenantAdmin; private User tenantAdmin;
@ -112,17 +116,14 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/device", device, Device.class); doPost("/api/device", device, Device.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?"); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1));
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 @Test
@ -136,9 +137,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(100, "/api/audit/logs?"); int expectedSize = loadedAuditLogsBefore.size() + 3;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test", loadedAuditLogsBefore.size() + 3, loadedAuditLogs.size()); Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test",
expectedSize, getAuditLogs(100, "/api/audit/logs?").size()));
} }
private List<AuditLog> getAuditLogs(int pageSize, String urlTemplate) throws Exception { private List<AuditLog> getAuditLogs(int pageSize, String urlTemplate) throws Exception {
@ -168,9 +170,9 @@ public class AuditLogControllerTest extends AbstractControllerTest {
savedDevice = doPost("/api/device", savedDevice, Device.class); savedDevice = doPost("/api/device", savedDevice, Device.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?"); Device finalSavedDevice = savedDevice;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals(11 + 1, loadedAuditLogs.size()); assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1));
} }
@Test @Test
@ -186,9 +188,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
tenantProfile.setName(tenantProfile.getName() + "(old)"); tenantProfile.setName(tenantProfile.getName() + "(old)");
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?"); TenantProfile finalTenantProfile = tenantProfile;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size()); 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 //cleanup
doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); 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.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -108,6 +109,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -115,6 +117,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
@ -322,6 +325,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType(cfType); calculatedField.setType(cfType);
calculatedField.setName("Test Calculated Field for " + entityId); calculatedField.setName("Test Calculated Field for " + entityId);
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
if (customConfiguration != null) { if (customConfiguration != null) {
calculatedField.setConfiguration(customConfiguration); calculatedField.setConfiguration(customConfiguration);
} else switch (cfType) { } else switch (cfType) {

7
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -1732,11 +1732,4 @@ public class DeviceControllerTest extends AbstractControllerTest {
assertThat(fifthDevice.getName()).isEqualTo("My unique device_2"); assertThat(fifthDevice.getName()).isEqualTo("My unique device_2");
} }
private Device createDevice(String name) {
Device device = new Device();
device.setName(name);
device.setType("default");
return doPost("/api/device", device, Device.class);
}
} }

6
application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java

@ -23,6 +23,7 @@ import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.awaitility.core.ThrowingRunnable;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
@ -86,6 +87,11 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
result -> result == expectedResult); result -> result == expectedResult);
} }
@Override
protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(assertion);
}
@Test @Test
public void testEdqsState() throws Exception { public void testEdqsState() throws Exception {
loginSysAdmin(); loginSysAdmin();

237
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -17,7 +17,11 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.awaitility.core.ThrowingRunnable;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -43,12 +47,21 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataPageLink;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo;
import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.DynamicValueSourceType;
@ -84,6 +97,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -1329,7 +1343,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
//assign dashboard //assign dashboard
doPost("/api/customer/" + savedCustomer.getId().getId().toString() doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
// check entity data query by customer // check entity data query by customer
User customerUser = new User(); User customerUser = new User();
@ -1454,6 +1468,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
return result; return result;
} }
protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) throws Throwable {
assertion.run();
}
private KeyFilter getEntityFieldEqualFilter(String keyName, String value) { private KeyFilter getEntityFieldEqualFilter(String keyName, String value) {
return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL); return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL);
} }
@ -1494,4 +1512,221 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
return nameFilter; return nameFilter;
} }
// --- findAvailableEntityKeysV2 tests ---
@Test
public void testFindAvailableKeysByQueryV2() throws Throwable {
// GIVEN — two devices matched by query; a third device should not be matched
var device1 = createDevice("Test device 1");
var device2 = createDevice("Test device 2");
var unmatchedDevice = createDevice("Unmatched device");
// unmatched device has unique keys that must NOT appear in the result
postTelemetry(unmatchedDevice.getId(), new BasicTsKvEntry(9000, new DoubleDataEntry("unmatchedTs", 999.0)));
postAttributes(unmatchedDevice.getId(), AttributeScope.SHARED_SCOPE, new StringDataEntry("unmatchedAttr", "nope"));
// device1: timeseries1 (Double) with two data points, and timeseries2 older data point
postTelemetry(device1.getId(), new BasicTsKvEntry(1000, new DoubleDataEntry("timeseries1", 10.0)));
postTelemetry(device1.getId(), new BasicTsKvEntry(2000, new DoubleDataEntry("timeseries1", 20.5)));
postTelemetry(device1.getId(), new BasicTsKvEntry(1000, new LongDataEntry("timeseries2", 100L)));
// device2: timeseries2 (Long) with a newer data point, and timeseries3 only on this device
postTelemetry(device2.getId(), new BasicTsKvEntry(3000, new LongDataEntry("timeseries2", 300L)));
postTelemetry(device2.getId(), new BasicTsKvEntry(5000, new DoubleDataEntry("timeseries3", 99.9)));
// device1: SHARED_SCOPE attributes
postAttributes(device1.getId(), AttributeScope.SHARED_SCOPE,
new BooleanDataEntry("sharedAttribute1", true), new DoubleDataEntry("sharedAttribute2", 3.14));
// device2: CLIENT_SCOPE attributes (saved via service to bypass API restriction)
attributesService.save(tenantId, device2.getId(), AttributeScope.CLIENT_SCOPE, List.of(
new BaseAttributeKvEntry(new JsonDataEntry("clientAttribute1", "{\"key\":\"val\"}"), System.currentTimeMillis()),
new BaseAttributeKvEntry(new BooleanDataEntry("clientAttribute2", false), System.currentTimeMillis())
)).get();
// device1 also has SERVER_SCOPE attributes (should be omitted by scope filter)
postAttributes(device1.getId(), AttributeScope.SERVER_SCOPE,
new StringDataEntry("serverAttribute1", "sv1"), new LongDataEntry("serverAttribute2", 42L));
// WHEN — query matches both devices; request timeseries + only SHARED and CLIENT attribute scopes
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("Test device");
EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, null);
EntityDataQuery query = new EntityDataQuery(filter, pageLink, List.of(), null, null);
// THEN
verifyAvailableKeysByQueryV2(() -> {
AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query,
true, true, List.of(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE), true);
assertThat(result.entityTypes()).containsExactly(EntityType.DEVICE);
// timeseries: keys collected from both devices, samples contain the freshest data points
assertThat(result.timeseries()).extracting(KeyInfo::key)
.containsExactly("timeseries1", "timeseries2", "timeseries3");
assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNotNull());
assertKeySample(result.timeseries(), "timeseries1", new DoubleNode(20.5), 2000); // from device1
assertKeySample(result.timeseries(), "timeseries2", new IntNode(300), 3000); // from device2 (newer)
assertKeySample(result.timeseries(), "timeseries3", new DoubleNode(99.9), 5000); // only on device2
// SERVER_SCOPE must be fully omitted from the response
assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE);
// SHARED_SCOPE: from device1 (alphabetical order)
assertThat(result.attributes().get(AttributeScope.SHARED_SCOPE))
.extracting(KeyInfo::key).containsExactly("sharedAttribute1", "sharedAttribute2");
assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute1", BooleanNode.TRUE);
assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute2", new DoubleNode(3.14));
// CLIENT_SCOPE: from device2 (alphabetical order)
assertThat(result.attributes().get(AttributeScope.CLIENT_SCOPE))
.extracting(KeyInfo::key).containsExactly("clientAttribute1", "clientAttribute2");
assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute1", JacksonUtil.toJsonNode("{\"key\":\"val\"}"));
assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute2", BooleanNode.FALSE);
});
}
@Test
public void testFindAvailableKeysByQueryV2_withoutSamples() throws Throwable {
// GIVEN
var device = createDevice("Test device");
postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0)));
postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0"));
// THEN
verifyAvailableKeysByQueryV2(() -> {
AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(
buildDeviceQuery("Test device"), true, true, null, false);
assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNull());
assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE))
.allSatisfy(ki -> assertThat(ki.sample()).isNull());
});
}
@Test
public void testFindAvailableKeysByQueryV2_timeseriesOnly() throws Throwable {
// GIVEN
var device = createDevice("Test device");
postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0)));
postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0"));
// THEN
verifyAvailableKeysByQueryV2(() -> {
AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(
buildDeviceQuery("Test device"), true, false, null, false);
assertThat(result.timeseries()).extracting(KeyInfo::key).contains("temperature");
assertThat(result.attributes()).isNull();
});
}
@Test
public void testFindAvailableKeysByQueryV2_attributesOnly() throws Throwable {
// GIVEN
var device = createDevice("Test device");
postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0)));
postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0"));
// THEN
verifyAvailableKeysByQueryV2(() -> {
AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(
buildDeviceQuery("Test device"), false, true, null, false);
assertThat(result.timeseries()).isNull();
assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE))
.extracting(KeyInfo::key).contains("firmware");
});
}
@Test
public void testFindAvailableKeysByQueryV2_noMatchingEntities() throws Throwable {
// THEN
verifyAvailableKeysByQueryV2(() -> {
AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(
buildDeviceQuery("NonExistentDevice_" + UUID.randomUUID()), true, true, null, true);
assertThat(result.entityTypes()).isEmpty();
assertThat(result.timeseries()).isEmpty();
assertThat(result.attributes()).isEmpty();
});
}
@Test
public void testFindAvailableKeysByQueryV2_assetUsesServerScopeOnly() throws Throwable {
// GIVEN
var asset = new Asset();
asset.setName("Test asset");
asset.setType("default");
asset = doPost("/api/asset", asset, Asset.class);
postAttributes(asset.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("location", "warehouse"));
// WHEN
var filter = new SingleEntityFilter();
filter.setSingleEntity(AliasEntityId.fromEntityId(asset.getId()));
var query = new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null);
// THEN
verifyAvailableKeysByQueryV2(() -> {
AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, false, true, null, false);
assertThat(result.entityTypes()).containsExactly(EntityType.ASSET);
assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SERVER_SCOPE);
assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE))
.extracting(KeyInfo::key).containsExactly("location");
});
}
@Test
public void testFindAvailableKeysByQueryV2_rejectsWhenNoKeyTypeRequested() throws Exception {
// WHEN / THEN
EntityDataQuery query = buildDeviceQuery("NonExistent");
doPostAsync("/api/v2/entitiesQuery/find/keys?includeTimeseries=false&includeAttributes=false",
query, 30_000L).andExpect(status().isBadRequest());
}
protected AvailableEntityKeysV2 findAvailableEntityKeysByQueryV2(EntityDataQuery query,
boolean includeTimeseries, boolean includeAttributes,
List<AttributeScope> scopes, boolean includeSamples) throws Exception {
StringBuilder url = new StringBuilder("/api/v2/entitiesQuery/find/keys?")
.append("includeTimeseries=").append(includeTimeseries)
.append("&includeAttributes=").append(includeAttributes)
.append("&includeSamples=").append(includeSamples);
if (scopes != null) {
for (AttributeScope scope : scopes) {
url.append("&scopes=").append(scope);
}
}
return doPostAsyncWithTypedResponse(url.toString(), query,
new TypeReference<>() {}, status().isOk());
}
private static void assertKeySample(List<KeyInfo> keys, String expectedKey, JsonNode expectedValue, long expectedTs) {
KeyInfo keyInfo = findKeyInfo(keys, expectedKey);
assertThat(keyInfo.sample()).isNotNull();
assertThat(keyInfo.sample().value()).isEqualTo(expectedValue);
assertThat(keyInfo.sample().ts()).isEqualTo(expectedTs);
}
private static void assertKeySample(List<KeyInfo> keys, String expectedKey, JsonNode expectedValue) {
KeyInfo keyInfo = findKeyInfo(keys, expectedKey);
assertThat(keyInfo.sample()).isNotNull();
assertThat(keyInfo.sample().value()).isEqualTo(expectedValue);
assertThat(keyInfo.sample().ts()).isGreaterThan(0);
}
private static KeyInfo findKeyInfo(List<KeyInfo> keys, String key) {
return keys.stream()
.filter(ki -> ki.key().equals(key)).findFirst().orElseThrow();
}
private static EntityDataQuery buildDeviceQuery(String deviceName) {
var filter = new DeviceTypeFilter();
filter.setDeviceTypes(Collections.singletonList("default"));
filter.setDeviceNameFilter(deviceName);
return new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null);
}
} }

7
application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java

@ -633,13 +633,6 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
deleteDifferentTenant(); deleteDifferentTenant();
} }
private Device createDevice(String name) {
var device = new Device();
device.setName(name);
device.setType("default");
return doPost("/api/device", device, Device.class);
}
private ResultActions getRelation(EntityRelation relation) throws Exception { private ResultActions getRelation(EntityRelation relation) throws Exception {
return doGet("/api/relation?" + return doGet("/api/relation?" +
"fromId=" + relation.getFrom().getId() + "fromId=" + relation.getFrom().getId() +

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

Loading…
Cancel
Save