Skip to content

fix(alerts): redact destination config from the testDestination response - #31634

Open
manerow wants to merge 3 commits into
mainfrom
fix/test-destination-redact-config
Open

fix(alerts): redact destination config from the testDestination response#31634
manerow wants to merge 3 commits into
mainfrom
fix/test-destination-redact-config

Conversation

@manerow

@manerow manerow commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Describe your changes:

Fixes #31633

POST /v1/events/subscriptions/testDestination returned the caller's own SubscriptionDestination objects, so the 200 response echoed config back verbatim, including the plaintext webhook authType.clientSecret / clientId (OAuth2), secretKey (bearer), and any credential-bearing headers or queryParams. The caller already holds that config, so nothing in the response needs to carry it.

The endpoint now returns one entry per requested destination, in request order, with config redacted and only statusDetails populated. Destinations whose delivery threw EventPublisherException were previously logged and dropped from the array; they are now returned with a Failed status and a reason, which both fixes a real bug (a connection failure produced no status banner at all) and makes the response length and order match the request.

On the UI side the destination rows locate their result by deep-equality on {type, category, config} against live form state, so they can no longer rely on the echoed config. The three call sites now keep the config they submitted and take only statusDetails from the response, via a new shared helper getDestinationsWithTestStatus in AlertsUtilPure.

Type of change:

  • Bug fix

High-level design:

The UI's deep-equality match is not only correlation, it is also a staleness guard: each row re-reads its values from live form state, so editing a field after a test correctly clears the banner. Attaching status by index prop instead would have regressed that behaviour and touched three child components plus their tests. Rebuilding destinationsWithStatus from the submitted array preserves child behaviour bit-for-bit while removing the dependency on the wire config entirely, since the submitted array is exactly what the server used to echo.

Redaction rather than masking: masking the secret fields would still break the deep-equality match, and precisely for the destinations that carry secrets. A SubscriptionDestination without config is the typed result this endpoint should have returned in the first place.

The response shape stays SubscriptionDestination[] (the field is simply absent), so no schema change and no client typing change is required.

Deliberately out of scope, listed in the issue: webhook auth fields still lack "format": "password" so PasswordEntityMasker is a no-op on them; Fernet.encryptWebhookSecretKey still gates on type == WEBHOOK leaving Slack/MsTeams/GChat auth in plaintext at rest; TestDestinationStatus.entity still carries the remote response body.

Tests:

Use cases covered

  • Admin clicks Test Destinations with an OAuth2 webhook: the response carries the status but no config, and the success banner still renders on the correct row.
  • A destination that cannot be reached (connection refused) now returns a Failed status with a reason instead of vanishing from the response.
  • Editing a destination field after a test still clears its banner (staleness guard preserved).

Unit tests

  • Added openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.test.ts (3 tests) covering config retention, positional pairing, and a short/missing result array.
  • Updated the mocked response in DestinationFormItem.test.tsx to the new config-less contract.
  • yarn test src/components/Alerts/DestinationFormItem src/components/Alerts/DestinationFormItemV2 → 65 passed.

Backend integration tests

  • Added EventSubscriptionResourceIT#test_testDestinationRedactsConfigAndReportsFailures, which posts a reachable OAuth2 webhook plus an unreachable one against an in-JVM HttpServer, and asserts the raw response body contains neither the sentinel secret nor clientSecret nor authType, that both entries come back in request order with config == null, and that the unreachable one reports Failed with a reason.
  • Verified as a genuine regression guard: against the unfixed server it fails on Response echoed the webhook client secret; against the fixed server it passes.
  • Full suite: mvn test -pl openmetadata-integration-tests -Dtest=EventSubscriptionResourceIT → 243 tests, 0 failures.

Ingestion integration tests

Not applicable (no ingestion changes).

Playwright (UI) tests

  • Extended playwright/e2e/Flow/NotificationAlerts.spec.ts with expect(testResult.config).toBeUndefined() to pin redaction end-to-end. The spec's existing length-2 assertion also becomes deterministic, since a transport failure no longer removes an entry.

Manual testing performed

Verification so far is the automated coverage above (backend IT run locally against the built server, jest, and the Playwright assertion); I have not yet driven this through a browser session end-to-end. Happy to attach a recording if reviewers would like one before merge.

UI screen recording / screenshots:

No visual change to the success path: the same banner renders in the same place, sourced from the submitted config instead of the echoed one. The one user-visible improvement is that an unreachable destination now renders a Failed banner where it previously rendered nothing.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR is linked to a GitHub issue via Fixes #31633 above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: not needed, no schema change (the response field is simply omitted).
  • I have added tests (unit / integration / Playwright as applicable) and listed them above.
  • I have added a test that covers the exact scenario we are fixing, and confirmed it fails without the fix.

Greptile Summary

The PR prevents /testDestination from echoing credential-bearing destination configuration while preserving request-order status correlation in the UI.

  • Returns one redacted result for each requested destination, including failed delivery attempts.
  • Reconstructs UI results from submitted destinations and response status details.
  • Adds backend integration, frontend unit, and Playwright regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/resources/events/subscription/EventSubscriptionResource.java Redacts destination configurations, preserves positional results, and converts publisher failures into returned failed statuses.
openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java Adds a status builder for delivery failures represented by a reason and timestamp.
openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.ts Reconstructs tested destinations from submitted configuration and positionally paired response statuses.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationFormItem.component.tsx Adapts the legacy alert form to config-less test responses.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx Adapts the react-hook-form alert form to config-less test responses.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/EventSubscriptionResourceIT.java Covers response redaction, request-order preservation, successful delivery, and connection failure reporting.
openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtilPure.test.ts Covers config retention, positional pairing, and missing response entries.

Sequence Diagram

sequenceDiagram
  participant UI as Alerts UI
  participant API as testDestination endpoint
  participant Dest as Destination publisher
  UI->>API: Submitted destinations with config
  loop In request order
    API->>Dest: sendTestMessage()
    Dest-->>API: Status or delivery exception
    API->>API: Build status and remove config
  end
  API-->>UI: Redacted destination results
  UI->>UI: Pair statuses with submitted configs by index
Loading

Reviews (3): Last reviewed commit: "fix(ui): sort imports in the AlertsUtilP..." | Re-trigger Greptile

Context used (4)

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

@manerow manerow added safe to test Add this label to run secure Github workflows on PRs To release Will cherry-pick this PR into the release branch labels Aug 17, 2026
@manerow manerow self-assigned this Aug 17, 2026
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 5021037eb401dbd06a1fe89ce09ca5ddc7ee5204 in Playwright run 32111680651, attempt 1.

✅ 981 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 55m 56s

⏱️ Max setup 3m 6s · max shard execution 20m 10s · max shard-job elapsed before upload 23m 25s · reporting 6s

🌐 205.78 requests/attempt · 2.42 app boots/UI scenario · 15.12% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 15.12% (convergence target: at most 15%).
  • Browser traffic was 205.78 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.42 per UI scenario (2499 boots / 1032 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 145 0 0 0 0 0
✅ Shard chromium-02 168 0 0 0 0 0
🟡 Shard chromium-03 151 0 1 0 0 0
✅ Shard chromium-04 150 0 0 0 0 0
✅ Shard chromium-05 155 0 0 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 14 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard import-export-01 26 0 0 0 0 0
✅ Shard ingestion-01 32 0 0 0 0 0
✅ Shard reindex-01 5 0 0 0 0 0
✅ Shard search-01 11 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 1 flaky test(s) (passed on retry)
  • Pages/Entity.spec.tsTier Add, Update and Remove (shard chromium-03, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

⚠️ UI Checkstyle passed — lint findings in changed files

🔍 ESLint findings in this PR's files — 0 error(s), 18 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 18 warning(s) across 4 changed file(s).

Count Rule
10 sonarjs/no-duplicate-string
3 react-hooks/exhaustive-deps
2 openmetadata-imports/no-lower-layer-page-imports
2 sonarjs/cyclomatic-complexity
1 i18next/no-literal-string
All findings
Location Rule Message
🟡 src/components/Alerts/DestinationFormItem/DestinationFormItem.component.tsx:52:9 react-hooks/exhaustive-deps The 'destinations' logical expression could make the dependencies of useMemo Hook (at line 97) change on every render. Move it inside the useMemo callback. Alte
🟡 src/components/Alerts/DestinationFormItem/DestinationFormItem.component.tsx:110:18 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/components/Alerts/DestinationFormItem/DestinationFormItem.test.tsx:65:56 i18next/no-literal-string disallow literal string: Icon
🟡 src/components/Alerts/DestinationFormItem/DestinationFormItem.test.tsx:101:31 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/components/Alerts/DestinationFormItem/DestinationFormItem.test.tsx:173:23 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 11 times.
🟡 src/components/Alerts/DestinationFormItem/DestinationFormItem.test.tsx:242:45 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 7 times.
🟡 src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:38:1 openmetadata-imports/no-lower-layer-page-imports Pages are route-level composition modules. Move the shared implementation/type to a lower layer instead of importing a page from here.
🟡 src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:67:9 react-hooks/exhaustive-deps The 'destinations' logical expression could make the dependencies of useMemo Hook (at line 99) change on every render. To fix this, wrap the initialization of '
🟡 src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:67:9 react-hooks/exhaustive-deps The 'destinations' logical expression could make the dependencies of useCallback Hook (at line 133) change on every render. To fix this, wrap the initialization
🟡 src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:78:20 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:149:55 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 4 times.
🟡 src/utils/Alerts/AlertsUtilPure.ts:37:1 openmetadata-imports/no-lower-layer-page-imports Pages are route-level composition modules. Move the shared implementation/type to a lower layer instead of importing a page from here.
🟡 src/utils/Alerts/AlertsUtilPure.ts:40:62 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 11 which is greater than 10 authorized.","cost":1,"secondaryLocations":[{"line":40,"column":61,"endLine":40,"endColumn"
🟡 src/utils/Alerts/AlertsUtilPure.ts:57:16 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/utils/Alerts/AlertsUtilPure.ts:425:66 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 16 which is greater than 10 authorized.","cost":6,"secondaryLocations":[{"line":425,"column":65,"endLine":425,"endColum
🟡 src/utils/Alerts/AlertsUtilPure.ts:428:16 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 13 times.
🟡 src/utils/Alerts/AlertsUtilPure.ts:429:22 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 13 times.
🟡 src/utils/Alerts/AlertsUtilPure.ts:442:21 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 5 times.

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

@github-actions

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 66%
66.85% (79912/119528) 51.25% (48774/95155) 52.23% (14601/27953)

@gitar-bot

gitar-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Redacts plaintext credentials and query strings from the testDestination response while returning failed deliveries with an explicit status. No issues found.

✅ 1 resolved
Security: Failure reason may echo endpoint URL with query-param credentials

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/events/subscription/EventSubscriptionResource.java:1463-1469
On the failure path, sendTestMessageToDestination stores e.getMessage() as the TestDestinationStatus.reason, and that message is built from eventPublisherFailedToPublish(WEBHOOK, ex.getMessage()) (GenericPublisher.sendTestMessage), where the underlying transport exception can include the target endpoint URL. Since this PR explicitly treats credential-bearing queryParams as sensitive (they are nulled out with config), a webhook whose credentials live in the endpoint query string could re-leak them through the un-redacted reason string. This is a narrow, best-effort concern rather than the main leak the PR fixes; consider sanitizing the reason to a generic connection-failure message or stripping the URL query before returning it.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

@manerow
manerow added this pull request to the merge queue Aug 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-08-18T21:45:25Z)

Blocked the queue: playwright-summary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs To release Will cherry-pick this PR into the release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backend: testDestination response echoes submitted destination config, including webhook auth secrets

3 participants