Skip to content

fix(secrets): only accept encoded credential values the entity already holds - #36016

Queued
TeddyCr wants to merge 11 commits into
mainfrom
fix/reject-foreign-credential-references
Queued

TeddyCr wants to merge 11 commits into
mainfrom
fix/reject-foreign-credential-references

Conversation

@TeddyCr

@TeddyCr TeddyCr commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Describe your changes:

The credential round trips in SecretsManager and PasswordEntityMasker assumed their input was either plaintext or a copy the server already held — nothing checked that. This PR closes the round trip: a credential field now accepts a plain value, or an encoded value the entity itself already holds; anything else is a client error. No legitimate round trip produces the rejected inputs, so no existing behavior depends on them.

I worked on the encrypt walk, the masker and their callers because every path that stores a credential validated its input's shape but never its provenance.

No linked issue — self-contained input validation, with the behavior change and its scope described below.

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality not work as expected)
  • Documentation

High-level design:

Three pieces:

  1. Encrypt validation — SecretsManager#requireOwnCredential, driven by a new CredentialPolicy. On strict paths (user create, bulk create) an encoded token is always rejected, and a secret: reference only when it falls inside the deployment's managed namespace (buildSecretId(true)) and outside the entity's accepted bases. A reference outside the namespace points into a vault the submitter manages themselves — those resolve at runtime in the ingestion framework and are never touched by the delete walker — so they stay allowed. Internal re-storage (a stored copy the server already holds) is exempt via CredentialPolicy.INTERNAL_RE_STORAGE.
  2. Unmask validation — PasswordEntityMasker#requireOwnStoredValue: a user update that carries an encoded value is accepted only when it matches, byte for byte, the value the field already holds, as stored or as resolved. A submission with nothing stored to compare against — a connection added where none was stored, a TEST_CONNECTION request with no stored service behind it — is still walked, and an encoded value with no owner is rejected there. The unmask walk now covers the fields the encrypt walk encrypts (only BasicAuthMechanism remains excluded), so bot JWT tokens are validated and masked server JWT credentials can be restored.
  3. Callers pick the policy — storeEntity(update=true) and the SecretsManagerUpdateService migrations re-store copies the server already holds under internal re-storage (safe only because the masker validated first); creates and bulk creates stay strict. WorkflowResource now types the request body before comparing it (an HTTP submission arrives as raw JSON), and builds the TEST_CONNECTION comparison base from the stored service's connection when the workflow does not exist yet, instead of from the submission itself — a base built from the submission validates it against itself.
  4. A second prepare pass in the same request is a re-storage — the PUT create-or-update path prepares twice: once at the resource boundary on the user's submission, and once more in the create pipeline on what the first pass encrypted. The PATCH pipeline runs the same shape: the masker pass validates the patched values against the stored copy, and the prepare pass after it stores what the masker accepted (EntityRepository#createPrepared / prepare(entity, update, reStore)). Without this, two things break: every PUT that creates a service with a plain password 400s on the server's own first-pass output, and a PATCH that renames a service rejects the connection's stored reference — an edit that never touched the connection — because the accepted bases are rebuilt from the new name. The resource boundary's own prepare stays strict; it runs before the create-or-update decision, so it cannot be keyed off the update flag.

Backward compatibility: the values that now get a 400 are exactly the encoded values the entity did not already hold. Plaintext submissions, PASSWORD_MASK round trips and references into the submitter's own vault behave as before. No schema, storage-format or migration changes.

A rejection returns a 400 naming the field and never echoes the submitted value — the value may itself be a credential, and it does not belong in the logs.

Known limitation: the bulk create-or-update update leg persists through the deferred-store path (EntityUpdater#updateWithDeferredStore → updateMany), which never runs the encrypt walk — a pre-existing shape this PR does not change. Services are still validated on that leg (their connection is encrypted at the resource-boundary prepare, which is strict), but entities whose credentials encrypt in storeEntity — ingestion pipelines, workflows, bot mechanisms — are not re-walked there. Covering that path is a follow-up.

Downstream companion: Collate #7960 moves query-runner, context-plugin and migration callers to the explicit internal re-storage overloads. The legacy overloads in this PR retain strict defaults.

Decision record: docs/decisions/2026-10-22-an-encoded-credential-needs-an-owner.md (ADR:2026-10-22-an-encoded-credential-needs-an-owner).

Tests:

Use cases covered

  • Creating or bulk-creating a service, ingestion pipeline, bot or workflow whose config carries an encoded token or a managed secret: reference it does not own returns a 400 naming the field; sending the plain value works as before.
  • A PUT create-or-update with a plain credential still creates the service, and the same PUT changing the credential to a new plain value or echoing the PASSWORD_MASK placeholder still saves it — the create pipeline's second prepare pass re-stores the first pass's own output instead of rejecting it.
  • A PATCH or PUT that does not touch a credential round-trips unchanged; an unmask-permitted user can echo back the resolved value the unmask handed them. A PATCH that changes a service's name keeps its stored credential resolvable.
  • A TEST_CONNECTION request submitted over HTTP can echo the tested service's own stored reference; the request is typed before the comparison so the stored service is found.
  • A reference into a vault the submitter manages themselves (outside the deployment's managed namespace) is still accepted and resolved at runtime.
  • Re-encrypting entities under a new secrets manager (the SecretsManagerUpdateService migrations) keeps working with already-encrypted values.

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added/updated:
    • openmetadata-service/src/test/java/org/openmetadata/service/secrets/DBSecretsManagerTest.java — strict encrypt rejects an encoded token (fails on main); internal re-storage round-trips an already-encrypted value.
    • openmetadata-service/src/test/java/org/openmetadata/service/secrets/ExternalSecretsManagerTest.java — runs on AWS/SSM/GCP: a service accepts its own stored reference echoed back; a reference outside the entity's own base is rejected; same pair for the workflow test-connection path (all four fail on main).
    • openmetadata-service/src/test/java/org/openmetadata/service/secrets/masker/PasswordEntityMaskerForeignCredentialTest.java (new) — rejects a token the entity does not hold, a reference outside its own base and an encoded value with no original to compare against; accepts the stored token echoed back, a plain value with no stored original and a reference into the submitter's own vault; validates the bot's JWT token; covers explicit strict and internal re-storage policies. Database-backed secrets also accept literal passwords beginning with secret:.
  • Existing provider/lifecycle/delete-walker fixtures pass unchanged — references outside the managed namespace remain a supported feature.

Backend integration tests

  • I added integration tests in openmetadata-integration-tests/ for new/changed API endpoints.
  • Files added/updated:
    • openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/SecretsManagerCredentialValidationIT.java (new) — end to end over the service PUT API: a create carrying an encoded token minted outside the request returns a 400 naming the field and never echoing the value; the same rejection on the update leg; the plain-value and PASSWORD_MASK round trips keep working (the rejection tests fail on main).
    • openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/SecretsManagerNullSecretIT.java — under the external secrets manager, a service created with a managed secret: reference it does not own is rejected with a 400 (fails on main); a PATCH that changes the service's name keeps its stored credential in place (fails on the first version of this PR, which re-validated the untouched connection against the new name's bases); a TEST_CONNECTION workflow submitted over HTTP echoes the tested service's own stored reference (same — the raw request body was never typed, so there was nothing to compare against).

Ingestion integration tests

  • I added/updated ingestion integration tests for connector changes.
  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • I added Playwright E2E tests under openmetadata-ui/.../ui/playwright/ for UI changes.
  • Not applicable (no UI changes).

Current head: e91fc1490ab6c07c4319c9879c1d0e2d97a0c55c.

  • Local validation after merging main and fixing functional regressions: 313 unit tests passed; 273 API test cases reported, 35 existing skips, zero failures/errors (238 executed). API suites: SecretsManagerCredentialValidationIT, SecretsManagerNullSecretIT, WorkflowTriggerPermissionsIT, WorkflowResourceIT, MetadataServiceResourceIT.
  • After restoring strict defaults in the final commit: all 11 PasswordEntityMaskerForeignCredentialTest cases passed. These include the explicit Collate re-storage overloads and strict rejection behavior.
  • Added regression coverage for encrypted foreign references in persisted workflow triggers, literal secret: passwords with the database secrets manager, and masked server JWT restoration. The first two failures were reproduced before the fixes.
  • Spotless and the impact-map check passed; the service, SDK and integration-test artifacts built successfully from the final commit for downstream validation.
  • PR CI is green: 38 passed checks, none failed or pending, including downstream Collate compatibility and service unit tests.
  • Extra full PostgreSQL + OpenSearch validation failed only on SearchRankingRelevanceIT.singularQueryFindsThePluralTable: 18,330 parallel-lane cases reported, one failure, zero errors, 1,771 skips. All other lanes passed. Recent main CI failed the identical assertion before these runs; it is not specific to this PR. All five SearchRankingRelevanceIT tests passed locally in isolation on both this head and clean merged-main commit 4a8b059.
  • Extra full MySQL + Elasticsearch validation also failed only on the same ranking assertion. Its uploaded parallel-lane JUnit reports contain 16,337 cases, one failure, zero errors and 1,600 skips across 441 reports; all other lanes passed. No search-ranking code or tests were changed in this PR. Both extra full-suite runs are recorded as failed, despite the PR checks being green.
  • Full service unit CI passed: 14,072 cases reported, one skip, zero failures/errors.
  • Additional local validation passed: all 119 selected MCP unit tests; 40 search integration cases; 51 cache/Redis integration cases reported, one skip, zero failures/errors.
  • The local full service run reported one PostgreSQL SSL setup error in UserDAOTest (zero assertion failures). Its isolated rerun passed all 42 cases; the full GitHub run also passed. This local attempt is recorded as failed, not as a clean full-suite pass.
  • Direct migration and version-aware storage tests were not added; those call sites preserve existing re-storage behavior.

Manual testing performed

No manual UI testing; this is a backend-only change. Automated local and remote evidence is listed above.

Functional review: merged current main and resolved the workflow-test and ingestion patch conflicts. Fixed persisted encrypted workflow-reference validation, database-manager plaintext classification, and masked JWT restoration. Independent review found no remaining blocking functional regressions. No cosmetic changes requested or added.

UI screen recording:

Not applicable — backend-only change, no UI surface touched.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue via Fixes #<issue-number> above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed. (no schema changes)
  • For a UI feature or feature task: as part of PR creation, I ran the PR build in Docker with sample data and embedded a verified GitHub-hosted recording plus build/health evidence above (or explained why this requirement is not applicable). (no UI changes)
  • I have added tests (unit / integration / Playwright as applicable) and listed them above.
  • Self-review: I reviewed the final diff against REVIEW.md (the pr-checklist skill runs it) and fixed every 🔴 finding, or answered it on the line below.
  • Decision check: this PR records its rule in docs/decisions/2026-10-22-an-encoded-credential-needs-an-owner.md.
  • I have added tests around the new logic.
  • For connector/ingestion changes: I updated the documentation. (no connector/ingestion changes)

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 3/5

The PR needs changes before merging because encrypted stored references bypass the new workflow trigger check.

Findings

  1. P1 Security Saved references skip the check ▶
  2. P2 Broad catches hide unexpected errors ▶

Summary

The PR limits encoded credentials to values the entity already holds, while preserving plain credentials and external-vault references.

  • Credential fields accept encoded values only when the entity already owns them.
  • Internal writes preserve credentials that passed the user-submission checks.
  • Connection-test workflows compare credentials with the saved service connection.
  • Changed array fields become replacement operations in ingestion patches.

TeddyCr explicitly defers the bulk update path that bypasses encryption and the downstream re-storage callers outside this repository.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Credential submitted] --> B[Check ownership]
  B --> C[Store secret or keep reference]
  C --> D[Encrypt field with Fernet]
  D --> E[(Saved workflow)]
  E --> F[Trigger reads encrypted field]
  F --> G[Reference prefix check skips token]
  G --> H[Worker fetches workflow]
  H --> I[Server decrypts reference]
  I --> J[Worker resolves referenced secret]
Loading

Reviews (6) · Last reviewed commit: "fix: address automated review findings (..." · Reviewed by Greptile

…y holds

The encrypt walk accepted any value a password field arrived with: a Fernet
token was decrypted with the server's key and stored as the submitting entity's
credential, and a secret: reference was stored verbatim. No legitimate round
trip feeds an encoded value into that walk from a user path - the masker
restores masked fields from the stored original, and unmask-permitted users echo
back the value the unmask handed them - so storing anything else leaves the
entity owning a value it was never given as a plain credential.

Two validation layers, a caller policy, and a re-storage pass keep those round
trips honest:

- SecretsManager.requireOwnCredential rejects, on strict paths (user create,
  bulk create), a Fernet token outright and a secret: reference when it falls
  inside the deployment's managed namespace and outside the entity's own
  accepted bases. References into a vault the submitter manages themselves stay
  allowed. Server-side re-storage passes CredentialPolicy with
  internalReStorage and is exempt.
- PasswordEntityMasker.requireOwnStoredValue accepts an encoded value on a user
  update only when it matches the field's stored or resolved value, and the
  unmask walk now covers the fields the encrypt walk encrypts
  (DO_NOT_UNMASK_CLASSES mirrors DO_NOT_ENCRYPT_CLASSES), so a bot's JWT token
  is validated too.
- Repositories and the SecretsManagerUpdateService migrations store their
  already-held copies under internal re-storage; creates and bulk creates stay
  strict. WorkflowResource builds the TEST_CONNECTION comparison base from the
  stored service's connection instead of the submission itself.
- The PUT create-or-update path prepares an entity twice: once at the resource
  boundary on the user's submission, and once more in the create pipeline on
  what the first pass encrypted. A strict policy cannot tell the first pass's
  own output from a foreign credential, so the second pass is marked as a
  re-storage (EntityRepository.createPrepared ->
  prepare(entity, update, alreadyPrepared)) and the service connection is
  re-stored as the server's copy. Without it, every PUT that creates a service
  with a plain password 400s on the server's own output.

A rejection returns a 400 naming the field and never echoes the submitted
value. Existing provider, lifecycle and delete-walker fixtures pass unchanged:
references outside the managed namespace remain a supported feature.

ADR: 2026-10-22-an-encoded-credential-needs-an-owner
@TeddyCr
TeddyCr requested a review from a team as a code owner October 23, 2026 01:55
@TeddyCr
TeddyCr requested a review from manerow October 23, 2026 01:55
@TeddyCr TeddyCr added the skip-pr-checks Bypass PR metadata validation check label Oct 23, 2026
Copilot AI balanced review requested due to automatic review settings October 23, 2026 01:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Oct 23, 2026
DBSecretsManager caches a JVM-wide singleton with the first caller's
configuration; other classes in the unit lane bootstrap it with no
prefix, so the hard-coded managed-reference fixture failed whenever
this class ran after them. The test now resets the singleton around
itself and rebuilds it from its own configuration.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 03:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…nd the HTTP workflow path

The PATCH pipeline's prepare pass runs after the masker pass, which has
already validated every encoded value against the stored copy, so it now
stores what the masker accepted instead of re-validating it against a
strict policy. Without that, a patch that changes a service's name rejects
the connection's stored reference, because the accepted bases are rebuilt
from the new name before the server reverts the rename.

A TEST_CONNECTION request that arrives over HTTP is typed at the top of
the resource's unmask, so the stored-service lookup finds the tested
service and the request can echo its own stored reference.

A connection a patch adds where none was stored is now walked by the
masker with nothing to compare against, so an encoded value it carries is
rejected instead of reaching the re-storage pass unvalidated.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@TeddyCr
TeddyCr enabled auto-merge October 23, 2026 22:43
return encryptContextPluginConnection(
connectionConfig,
pluginName,
CredentialPolicy.strictWithReferenceBases(

@yan-3005 yan-3005 Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this default is now strict, Collate's context plugins and query runner will start getting 400s once this lands.

ContextPluginRepository.encryptConnection copies the stored secrets into the payload on update (an OAuth plugin's refresh token always gets carried over, since the form never sends it) and then calls this overload. On the db secrets manager that stored value is a Fernet token, so editing any OAuth plugin fails. On AWS-style managers a rename fails too, because the stored reference sits under the old name. QueryRunnerConfigService re-saves already-encrypted configs through encryptQueryRunnerConfig in a similar way.

Collate builds against OSS main, so the Collate companion PR needs to land together with this one.


for (IngestionPipeline ingestionPipeline : entities) {
if (secretsManager != null) {
// The batch store is a user create path: encoded values it does not own are rejected.

@yan-3005 yan-3005 Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bulk update leg (updateWithDeferredStore) still stores pipelines, workflows and bot mechanisms without this check, so a foreign token can still get in that way. Could we cover that path in this PR as well, so the fix is complete?

- [high] AWS SSM parameter ARNs bypass managed-namespace validation (F-7af61dec34)
- [high] Entity-name prefixes allow another entity's managed credentials (F-fa978fcf80)
- [medium] Secrets-manager migration updates workflows through the ingestion-pipeline DAO, so workflows are never migrated (F-814266bdd6)
- [medium] Owned-reference check is a prefix match, so an entity can claim the secret reference of any entity whose name extends its own (F-b0f089c900)

Automated by pr-supervisor using glm-5.3-flash:cloud via ollama; reviewed by gpt-6.1-sol.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 05:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

- [high] TEST_CONNECTION echo accepts a guessable reference to the named service's secret and can run it against an attacker-controlled host (F-31f7346e4b)
- [high] Saved AWS credentials block saves (G-a6034a0b52)
- [medium] If-Match PATCH stores the masker-decrypted workflow or bot credential in plaintext, bypassing storeEntity (F-11490a8d77)
- [medium] AWS SSM trims leading/trailing spaces, so a space-padded reference bypasses the managed-namespace classification (F-16f8a4309c)

Automated by pr-supervisor using glm-5.3-flash:cloud via ollama; reviewed by gpt-6.1-sol.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 06:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

- [high] If-Match PATCH on an ingestion pipeline persists masker-decrypted sourceConfig credentials as plaintext (F-b2ac1351f1)
- [high] Stored TEST_CONNECTION workflows can still carry and run another service's reference; the trigger and the migration do not revalidate them (F-e676d413a4)
- [high] Rejected edits change bot credentials (G-48db171092)
- [medium] Unmask errors lose their cause (G-281daf48f4)
- [medium] Require EditAll on the service only when the service exists. Otherwise fall back to the pipeline-create rule.: (G-bbd8521720)

Automated by pr-supervisor using glm-5.3-flash:cloud via ollama; reviewed by gpt-6.1-sol.
@TeddyCr
TeddyCr requested a review from a team as a code owner October 28, 2026 07:59
@TeddyCr
TeddyCr requested a review from ulixius9 October 28, 2026 07:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Copilot AI balanced review requested due to automatic review settings October 28, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@TeddyCr
TeddyCr added this pull request to the merge queue Oct 29, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 29, 2026
@github-actions github-actions Bot added the merge conflict Conflicts with the latest main; removed from the merge queue label Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — merge_conflict (2026-10-29T10:23:45Z)

The entry left the queue before it was built, so no checks ran against it.

Conflicts with the latest main — rebase, then re-queue.

# Conflicts:
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java
Copilot AI balanced review requested due to automatic review settings October 29, 2026 16:16
@TeddyCr
TeddyCr enabled auto-merge October 29, 2026 16:16
@github-actions github-actions Bot removed the merge conflict Conflicts with the latest main; removed from the merge queue label Oct 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@TeddyCr
TeddyCr removed request for a team, manerow and ulixius9 October 29, 2026 16:18
authorizeNamedServiceEdit(securityContext, testRequest);
}
Workflow originalWorkflow;
if (WorkflowType.TEST_CONNECTION.equals(workflow.getWorkflowType())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Non-admin test of a not-yet-created service NPEs on null original

In this commit, buildFromOriginalServiceConnection returns storedServiceConnection(workflow) for a new workflow, and that returns null when the named service doesn't exist yet. That is the add-service flow, where the user tests a connection before saving the service. For a non-admin, non-bot caller, unmask then runs requireSecretsReenteredOnRetarget(workflow, originalWorkflow). In that method, converted.getRequest() instanceof TestServiceConnectionRequest is true, so the next condition, original.getRequest(), is evaluated on null and throws a NullPointerException. The caller gets a 500 instead of a test-connection workflow. Before this commit, originalWorkflow was never null on this path because it fell back to a converted copy. The authorization comment says this flow intentionally keeps the pipeline-create rule, so it is meant to work.

Fix: skip the retarget check when there is no stored original. With nothing stored, there is no secret to borrow, and the masker already rejects encoded values that have no stored base.

Guard the retarget check against a missing comparison base:

originalWorkflow = buildFromOriginalServiceConnection(workflow);
if (originalWorkflow != null && !readsStoredSecrets(securityContext)) {
  requireSecretsReenteredOnRetarget(workflow, originalWorkflow);
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Oct 29, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 3 closed / 4 findings

🔴 High risk · Credential ownership checks and TEST_CONNECTION authorization govern stored-secret use; a flaw could expose another service’s credentials or block legitimate updates.

Tightens credential handling so encoded tokens and managed secret references are accepted only when the entity already holds them, with a second-pass re-store for internal writes. The review has an open important finding: a non-admin test of a not-yet-created service's connection throws a NullPointerException when no stored original exists in WorkflowResource, returning a 500 instead of running the test, and the retarget check must skip when there is no stored original.

Resolved this session: renaming a service on an external secrets manager now returns 400, testing a new service's connection now requires EditAll on a missing service, and the new strict defaults on Collate-facing overloads now have test coverage.

⚠️ Bug: Non-admin test of a not-yet-created service NPEs on null original

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:748-754 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:769-775 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:817-818 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:837-844

In this commit, buildFromOriginalServiceConnection returns storedServiceConnection(workflow) for a new workflow, and that returns null when the named service doesn't exist yet. That is the add-service flow, where the user tests a connection before saving the service. For a non-admin, non-bot caller, unmask then runs requireSecretsReenteredOnRetarget(workflow, originalWorkflow). In that method, converted.getRequest() instanceof TestServiceConnectionRequest is true, so the next condition, original.getRequest(), is evaluated on null and throws a NullPointerException. The caller gets a 500 instead of a test-connection workflow. Before this commit, originalWorkflow was never null on this path because it fell back to a converted copy. The authorization comment says this flow intentionally keeps the pipeline-create rule, so it is meant to work.

Fix: skip the retarget check when there is no stored original. With nothing stored, there is no secret to borrow, and the masker already rejects encoded values that have no stored base.

Guard the retarget check against a missing comparison base
originalWorkflow = buildFromOriginalServiceConnection(workflow);
if (originalWorkflow != null && !readsStoredSecrets(securityContext)) {
  requireSecretsReenteredOnRetarget(workflow, originalWorkflow);
}
✅ 3 closed
✅ Bug: Renaming a service on an external secrets manager now fails with 400

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ServiceEntityRepository.java:174-187 📄 openmetadata-service/src/main/java/org/openmetadata/service/secrets/SecretsManager.java:148-155 📄 openmetadata-service/src/main/java/org/openmetadata/service/secrets/SecretsManager.java:772-786
Under an external secrets manager a service's stored password is a secret: reference built from the service's name at creation (e.g. secret:/<cluster>/database/oldname/...). Renaming is supported (ServiceEntityRepository.postUpdate rewrites policy conditions when original.getName() != updated.getName()). Here is what happens on a PATCH that changes name:

  1. restorePatchSecrets unmasks against the stored copy and accepts the echoed reference.
  2. prepareInternal(updated, true) runs the strict prepare(service, update).
  3. The default encryptServiceConnectionConfig overload only accepts references under buildSecretId(true, serviceType, service.getName()), which is the new name.
  4. The service's own reference is inside the managed namespace but outside that base, so requireOwnCredential throws a 400.

Any service renamed before this PR is also affected: every later PATCH or PUT that echoes its stored references fails the same way. This contradicts the ADR's claim that masked round trips behave as before.

Fix: the update path has already validated the value in the masker, so it should not reject the entity's own stored references. Either pass INTERNAL_RE_STORAGE when update == true, as the other repositories do in storeEntity, or add the original name's base to the accepted reference bases. Add a test that renames a service with an external manager configured.

✅ Bug: Testing a new service's connection now needs EditAll on a missing service

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:669-683 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:707-714
unmask (create and createOrUpdate) and authorizeTestConnection now call authorizeNamedServiceEdit whenever the request has a serviceName. The ingestion-pipeline-create fallback is gone. The add-service UI always sends that name: AddServicePage passes data={serviceConfig}, ConnectionConfigForm passes serviceName={data?.name}, and TestConnection puts serviceName into the CreateWorkflow request. When the service doesn't exist yet, ResourceContext.resolveEntity swallows EntityNotFoundException and evaluates EDIT_ALL against a null entity. Owner-style or other conditional rules can't match a null entity, so a non-admin user who may create services and pipelines, but has no blanket EditAll on services, now gets 403 when they POST the test-connection workflow for a service they are still creating.

The credential-borrowing risk in the ADR v4 amendment only exists when a stored service connection exists to echo. buildFromOriginalServiceConnection/storedServiceConnection already return null in that case, so the masker rejects any encoded value. Fix: require EditAll only when findStoredService(testRequest) returns a service, and keep authorizeIngestionPipelineCreate (or the old ANY) when the named service does not exist. Add an IT that tests the connection of a service that hasn't been created, as a non-admin with pipeline-create permission.

✅ Quality: New strict defaults on Collate-facing overloads have no test

📄 openmetadata-service/src/main/java/org/openmetadata/service/secrets/SecretsManager.java:246-253 📄 openmetadata-service/src/main/java/org/openmetadata/service/secrets/SecretsManager.java:326-331 📄 openmetadata-service/src/test/java/org/openmetadata/service/secrets/masker/PasswordEntityMaskerForeignCredentialTest.java:76-90
This commit changes the default policy of the 3-arg encryptQueryRunnerConfig and the 2-arg encryptContextPluginConnection from INTERNAL_RE_STORAGE to strictWithReferenceBases(...), and moves the existing test onto the explicit INTERNAL_RE_STORAGE overload. That leaves the new default behavior with no tests. Nothing checks that a default call rejects a foreign Fernet token, or that it still accepts a reference under the entity's own base (.../DatabaseService/<svc>/queryrunner/<type>, .../contextPlugin/<name>). If the reference base is built wrong, it would silently reject the entity's own stored references, which is exactly the contract Collate is being asked to migrate onto. Add a test that calls the default overloads with a foreign token (expect a 400) and with the entity's own managed reference (expect it to be accepted).

🤖 Prompt for agents
Code Review: Tightens credential handling so encoded tokens and managed secret references are accepted only when the entity already holds them, with a second-pass re-store for internal writes. The review has an open important finding: a non-admin test of a not-yet-created service's connection throws a NullPointerException when no stored original exists in `WorkflowResource`, returning a 500 instead of running the test, and the retarget check must skip when there is no stored original.
  
  Resolved this session: renaming a service on an external secrets manager now returns 400, testing a new service's connection now requires EditAll on a missing service, and the new strict defaults on Collate-facing overloads now have test coverage.

1. ⚠️ Bug: Non-admin test of a not-yet-created service NPEs on null original
   Files: openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:748-754, openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:769-775, openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:817-818, openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java:837-844

   In this commit, `buildFromOriginalServiceConnection` returns `storedServiceConnection(workflow)` for a new workflow, and that returns `null` when the named service doesn't exist yet. That is the add-service flow, where the user tests a connection before saving the service. For a non-admin, non-bot caller, `unmask` then runs `requireSecretsReenteredOnRetarget(workflow, originalWorkflow)`. In that method, `converted.getRequest() instanceof TestServiceConnectionRequest` is true, so the next condition, `original.getRequest()`, is evaluated on `null` and throws a NullPointerException. The caller gets a 500 instead of a test-connection workflow. Before this commit, `originalWorkflow` was never null on this path because it fell back to a converted copy. The authorization comment says this flow intentionally keeps the pipeline-create rule, so it is meant to work.
   
   Fix: skip the retarget check when there is no stored original. With nothing stored, there is no secret to borrow, and the masker already rejects encoded values that have no stored base.

   Fix (Guard the retarget check against a missing comparison base):
   originalWorkflow = buildFromOriginalServiceConnection(workflow);
   if (originalWorkflow != null && !readsStoredSecrets(securityContext)) {
     requireSecretsReenteredOnRetarget(workflow, originalWorkflow);
   }

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Display: compact → Showing compact verification results.

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

@TeddyCr
TeddyCr added this pull request to the merge queue Oct 29, 2026
Any commits made after this event will not be merged.
@sonarqubecloud

Copy link
Copy Markdown

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-29T20:54:42Z)

Blocked the queue: integration-tests-postgres-opensearch

@TeddyCr
TeddyCr added this pull request to the merge queue Oct 29, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-30T05:59:48Z)

Blocked the queue: playwright-summary

  • Maven Collate Tests — maven-collate-ci
  • Postgresql PR Playwright E2E Tests — playwright-summary, playwright / playwright-ci (chromium-10), playwright / playwright-ci (chromium-04), playwright / playwright-ci (chromium-02), playwright / playwright-ci (chromium-06)

Failed Playwright tests:

  • Features/OntologyNativeOperations.spec.ts › retries failed review submission across tabs using the same captured release
  • Features/OntologyNativeTreeShell.spec.ts › keeps both parent placements, one selection and all header controls at 924 pixels
  • Features/OntologyReview.spec.ts › manages reviewers with reload persistence and locks settings during an open review
  • Flow/ProductTour.spec.ts › walks the Classic pages on recorded data and returns to Home

@anuj-kumary
anuj-kumary added this pull request to the merge queue Oct 30, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

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

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants