Accept JsonNode and use JacksonUtil.treeToValue() instead of direct
@RequestBody DeviceInstalledItemDescriptor. The @JsonTypeInfo on the
parent interface caused Spring's message converter to fail when
deserializing the concrete class directly.
Backend:
- Expand DeviceInstalledItemDescriptor with createdEntityIds and dashboardId
- Add POST /api/iot-hub/device/register endpoint to save installed item
- Implement reverse-order entity deletion for DEVICE uninstall
- Update DEVICE case in updateItemVersion to clean old entities
Frontend:
- Expand DeviceInstalledItemDescriptor TS model
- Add registerDeviceInstall API method
- Wire wizard to collect entity IDs and register after completion
- Enable dashboard navigation for installed device packages
- Add getInstalledItemsCount endpoint through full stack (controller → DAO)
with optional itemType filter
- Add getInstalledItemsCount to UI IotHubApiService
- Home page: show "Installed Items: N" stroked button at top-right when
count > 0, white bg, primary border, card shadow; navigates to installed items
- Count updates on install/delete via API re-fetch and local decrement
- Move IoT Hub menu item right after Dashboards in sidebar
- Add @Input installedItem to card component with install/installed/update states
- Add updateClick output to card, wire update dialog from browse component
- Add itemType filter to getInstalledItems across full stack (controller → DAO)
- Fetch installed solution templates on browse page when tab selected
- Pass installedItem to detail dialog from browse component
- Reload items on install/update dialog close for solution templates
- Add MpItemVersionResource model and resources field to MpItemVersionView
- Add image carousel (ngx-hm-carousel) to detail dialog for solution templates
- Carousel: screenshots preferred over icons, autoplay 3s, dots below images
- Add SolutionService with install/delete/rollback flow, entity provisioning
(customers, assets, devices, dashboards, edges, rule chains, CFs, users),
telemetry emulators, instructions templating, and public customer support
- Add SolutionInstallDialogComponent in home/components/solution with markdown
details display and "Go to main dashboard" navigation
- Update IoT Hub install dialog to open solution dialog on template success
- Add SOLUTION_TEMPLATE type support across frontend (item cards, detail dialog,
installed items table with dashboard entity mapping)
- Add solution instructions button to item detail dialog for installed templates
- Update edge detail URLs and dashboard links for correct CE routing
Route DataAccessException and PersistenceException (including bare
ConstraintViolationException) to a unified handler that extracts the
constraint name and returns "Constraint violation: <name>" instead of
the raw PSQLException message. Other DB errors continue to return the
generic "Database error" response.
Adds DaoUtil.extractConstraintViolation helper and an integration test
that verifies no SQL details leak when an FK constraint is violated.
Keep both save-time validation (Oauth2ClientDataValidator) and runtime
re-validation as defense-in-depth: DNS records can change between config
save and OAuth2 login, creating a TOCTOU gap.
- Use kebab-case 'report-only' in web-ui configs to match thingsboard.yml
- Add log.warn for unrecognized X-Frame-Options values in customizer
- Replace @Configuration with @Component on HttpSecurityHeadersProperties
- Add comment explaining '!== false' vs truthiness pattern in server.ts
- Extract shared parseHostEntries() to deduplicate setAllowedHosts/setAdditionalBlockedHosts
- Add isHostnameAllowed() and propagate hostname allow-list check in resolver
- Move OAuth2 custom mapper URL SSRF validation to save-time (Oauth2ClientDataValidator)
- Remove runtime SSRF checks from CustomOAuth2ClientMapper and GithubOAuth2ClientMapper
(custom URL now validated at save; GitHub emailUrl is server config, not user input)
- Replace example.com with 8.8.8.8 in resolver test to avoid DNS dependency
Fix security issues from penetration test report:
- M2: Add configurable X-Frame-Options and CSP headers (disabled by default)
- L2: Add X-Content-Type-Options and Referrer-Policy headers (enabled by default)
- L3: Make CORS allowed-origin-patterns configurable via TB_CORS_* env vars
Root cause: ThingsboardSecurityConfiguration called .disable() on the entire
HeadersConfigurer, which removed ALL security headers including Cache-Control.
Fix uses defaultsDisabled() + selective header enablement via a new
HttpSecurityHeadersCustomizer component.
Both Spring Boot (tb-node) and Express.js (web-ui) share the same
SECURITY_HEADERS_* environment variables for consistent configuration
across monolith and microservice deployments.
Add SsrfSafeAddressResolverGroup that validates resolved IPs at Netty
connection time, eliminating the TOCTOU gap where DNS rebinding domains
resolve to safe IPs during validation but to private/metadata IPs at
connection time. Disable HTTP redirects in TbHttpClient to prevent
redirect-based SSRF bypass.
Add allow-list support (SSRF_ALLOWED_HOSTS) to SsrfProtectionValidator
so customers with IoT devices on private networks can whitelist specific
addresses or CIDR ranges while keeping SSRF protection enabled.
Add SSRF validation to MS Teams webhook, custom OAuth2 mapper, and
GitHub OAuth2 mapper endpoints. Log a warning when SSRF protection is
disabled.
- Refactor update/delete endpoints to use installed item's own ID instead of marketplace itemId
- Add SHA-256 checksum comparison to detect local entity modifications before update
- Support force update to skip checksum check when user confirms overwrite
- Add per-type update methods (widget, dashboard, calculated field, rule chain)
- Add entityId field to CalculatedFieldInstalledItemDescriptor
- Replace checkForUpdates with getItemsPublishedVersions API
- Replace getInstalledItemInfos with getInstalledItemIds returning List<UUID>
- Remove unused IotHubInstalledItemInfo class and resolver
- Set metadata version from saved rule chain in install/update flows
- Add entity-modified confirmation dialog in update UI
Static fields RESOURCE_ID_3303_12_5700_TS_0/TS_1 in Lwm2mTestHelper are never
reset between test runs. On CI retries the await() at the start of the test
passes immediately (both timestamps are still > 0 from the previous run), so
the telemetry query uses stale timestamps and the second await() times out.
Fix: add @Before that resets both timestamps to 0 before each test method,
which was already correctly implied by the unused @Before import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Leshan's NotificationDataStore.toKey() can throw NPE when the server reference
is null during CoAP observe-relation cleanup on client shutdown (race condition).
This NPE was caught by the outer try-catch in startUpdating(), which prevented
leshanClient.start() from being called, leaving the simulated device stuck in
UPDATING state and causing the awaitility timeout in the OTA integration test.
Fix: wrap leshanClient.stop(false) in its own try-catch so that a Leshan
internal exception during stop does not abort the subsequent client restart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make setupSsl package-private so the test can call it directly, ensuring
tests exercise the real production code path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add RSA_4096 and EC_P256 alongside RSA_2048 and EC_P384
- Parameterize encrypted key tests (RSA-only, EC encrypted keys
are a pre-existing PemSslCredentials limitation)
- 14 test scenarios total
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add EC_P384 key type alongside RSA_2048
- Parameterize separateCertAndKeyFiles and combinedPemFile tests
- Write private keys in PKCS#8 format for EC compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use forPort(0) instead of TestSocketUtils for random port
- Replace manual poll loop with Awaitility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests mirror EdgeGrpcService.setupSsl() using PemSslCredentials:
- Separate cert and key files (existing behavior)
- Combined PEM file (cert + key)
- Encrypted private key with key_password
- Error when no private key found
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep privateKeyFile.pem as the default so existing users with
separate cert/key file configs are not affected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reuse PemSslCredentials (already handles combined PEM, separate files,
and encrypted keys) instead of duplicating PEM parsing logic.
Wire it into gRPC via GrpcSslContexts + KeyManagerFactory.
- Make private_key config optional (default empty) for combined PEM
- Add key_password config for encrypted private keys
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- TimeseriesServiceNoSqlTest.shouldSaveEntryOfEachTypeWithTtl: await
tsService.save() with bounded .get(MAX_TIMEOUT, TimeUnit.SECONDS)
- EntityServiceTest.testFindTenantTelemetry: await timeseriesService.save()
and attributesService.save() with .get(TIMEOUT, TimeUnit.SECONDS) to
prevent both the race condition and an indefinite hang
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add update item version endpoint and service method (backend)
- Add update dialog component with confirm/updating/success/error states
- Add check for updates feature in installed items table
- Show update button on browse cards and detail dialog when update available
- Add route resolver for installed item infos shared across pages
- Improve deserialization: use ignoreUnknownFields, root cause error messages
- Refactor rule chain install to parse ruleChain and metadata separately
- Full entity stack: IotHubInstalledItem with ID, descriptors, JPA entity, DAO (extends JpaAbstractDao), service
- Controller endpoints: install, list (paginated), list infos, get by itemId, delete
- Install flow saves descriptor with entity IDs, delete cascades to installed entities
- Frontend installed items page with search, pagination, type chips, action buttons
- Browse page shows "Installed" label on cards, detail dialog shows "Open details" for installed items
- Installed item infos fetched on browse page init to track installation state
EntityViewControllerTest was importing MQTT_PORT from AbstractMqttIntegrationTest,
a static final field initialized once per JVM. When running in the same Surefire
fork alongside other test classes that also use this constant (e.g. MqttGatewayRateLimitsTest,
DeviceEdgeTest), each class gets a different Spring context key but all try to bind
MqttTransportService to the same port, causing BindException.
Fix: define a private static MQTT_PORT/MQTT_URL directly in EntityViewControllerTest
so its Spring context gets its own independently allocated port.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests like testFindTenantsByTitle create 261 tenants and delete them via
deleteEntitiesAsync, which only waits for HTTP responses — not housekeeper
completion. Each tenant deletion submits ~30 TenantEntitiesDeletionHousekeeper
tasks (~7800 tasks total), which cascade further. The teardown's deleteTenant
then waits for lag==0 with a 90s timeout, which is insufficient for this
backlog and causes ConditionTimeoutException.
Fix: add awaitHousekeeperDrained() (5-min timeout) called at the start of
teardownWebTest so any pending housekeeper work from the test body drains
before per-tenant teardown deletions begin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
doGet/doGetAsyncTyped assert HTTP 200 internally, so any non-200 response
throws AssertionError which Awaitility re-throws immediately instead of
continuing to poll.
Add .ignoreExceptions() to three additional await() polling loops that
call HTTP helpers:
- AbstractMqttV5ClientSparkplugAttributesTest: two doGetAsyncTyped calls
polling for attribute keys after NBIRTH/DBIRTH
- AbstractMqttAttributesIntegrationTest: doGetAsyncTyped polling for
attribute values after client publish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
doGet(url, Class) asserts HTTP 200 internally, so when the Sparkplug
device hasn't been created yet the method throws AssertionError instead
of returning null. Awaitility propagates Error immediately rather than
continuing to poll, causing the test to fail after ~3 s instead of
retrying for up to 200 s.
Add .ignoreExceptions() to both await() calls in
connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices and
connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices so
that a transient 404 is treated as "condition not yet met" and polling
continues as intended.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the MQTT transport closes the session on RPC delivery timeout,
the server re-queues the RPC asynchronously. The test was asserting
the RPC status immediately after the client disconnect latch fired,
before the server-side re-queuing had completed — so the status was
still SENT instead of the expected QUEUED.
Replace the direct doGet assertion with an Awaitility poll that waits
up to DEFAULT_WAIT_TIMEOUT_SECONDS for the status to become QUEUED.
Fixes flaky: MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Execute RPC is sent to FW Update resource (/5/0/2), the test client's
startUpdating() scheduled the client stop with 0 delay. This caused a race
where the client stopped before the CoAP Execute response (2.04 Changed)
was delivered to the server, resulting in RequestCanceledException and
INTERNAL_SERVER_ERROR instead of CHANGED.
Adding a 1-second delay before leshanClient.stop() ensures the CoAP
response is transmitted and received before the client disconnects, fixing
the flaky testExecuteUpdateFWById_Result_CHANGED test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use ID token claims as the source of truth for Apple OAuth2 attributes
- Added Apple mapper type to OAuth2 client data validation
- Consolidated duplicated validation logic for BASIC, GITHUB, and APPLE mapper types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The lastCommit field was shared across all repository keys, causing
MissingObjectException when multiple repositories were registered.
When onUpdate fired for repo A it overwrote lastCommit, and subsequent
listFiles/getFileContent calls for repo B used repo A's commit whose
tree objects don't exist in repo B's object database.
Changed to a per-key Map<String, RevCommit> so each repository's
resolved commit is stored and retrieved independently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>