Skip to content

fix(security): restrict service connection reads to bots and editors - #36219

Merged
mohityadav766 merged 2 commits into
mainfrom
security/ghsa-46f4-metadata-secret-mask
Oct 26, 2026
Merged

mohityadav766 merged 2 commits into
mainfrom
security/ghsa-46f4-metadata-secret-mask

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Describe your changes:

Harden how service connections are returned and stored.

  • A service connection is returned in full only to bots, masked to callers who can edit the
    service
    (admins and delegated operators with EDIT_ALL), and not at all to anyone else.
    The masked-vs-null choice is gated on edit permission, not isAdmin(), so delegated operators keep
    managing connection details while view-only users receive no connection.
  • The built-in OpenMetadata service keeps only host/port/auth provider — it no longer stores the
    Elasticsearch sink or the ingestion-bot JWT. Ingestion builds its own connection on the fly and
    reindexing reads the search config from the application config.
  • The server-connection JWT is masked for non-bot readers.
  • A migration drops those fields from existing metadata service rows and their version history
    (entity_extension), in both MySQL and Postgres.

The visibility rule is recorded in docs/decisions/2026-10-26-service-connection-visibility-follows-edit-permission.md.

Type of change:

  • Bug fix

Tests:

  • PasswordEntityMaskerTest — the server-connection JWT is masked and restored on write.
  • MetadataServiceResourceIT — editor=masked / viewer=no connection / bot=full; the built-in
    service has no Elasticsearch sink or stored JWT.

RetriggerView in GreptileConfidence Score: 4/5

The remaining comment-style requirement must be satisfied before merging.

Findings

  1. P2 Comments repeat the assertions ▶

Summary

Service connections are returned in full to bots, masked to editors, and hidden from viewers.

  • Service connections are visible only to bots and callers who can edit the service.
  • The built-in OpenMetadata service keeps only the connection details it needs.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Read service connection] --> B{Bot?}
  B -->|Yes| C[Return full connection]
  B -->|No| D{Can edit the service?}
  D -->|Yes| E[Return masked connection]
  D -->|No| F[Return null connection]
Loading

Reviews (6) · Last reviewed commit: "test: align messaging connection visibil..." · Reviewed by Greptile

@mohityadav766
mohityadav766 requested a review from a team as a code owner October 26, 2026 09:06
@mohityadav766
mohityadav766 requested a review from pmbrull October 26, 2026 09:06
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Oct 26, 2026
@mohityadav766
mohityadav766 force-pushed the security/ghsa-46f4-metadata-secret-mask branch from 47bda69 to 6467045 Compare October 26, 2026 09:21
@mohityadav766 mohityadav766 changed the title fix(security): do not expose service connection secrets to non-bot users fix(security): restrict service connection visibility to bots and admins Oct 26, 2026
JsonUtils.convertValue(asAdmin.getConnection().getConfig(), AmundsenConnection.class);
assertEquals("*********", adminConn.getPassword());

// VIEW_BASIC user: no connection at all.

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.

P2 Comments repeat the assertions

The new comments label the results asserted directly below them, such as “no connection at all” before assertNull(asUser.getConnection()). The repository requires comments to explain why rather than restate obvious code. Remove these labels or explain a non-obvious reason for the cases. This requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@mohityadav766 mohityadav766 self-assigned this Oct 26, 2026

@manerow manerow 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.

One gap in the migration, see inline. Rest looks good.

Comment on lines +696 to +700
UPDATE metadata_service_entity
SET json = JSON_REMOVE(json, '$.connection.config.elasticsSearch', '$.connection.config.securityConfig')
WHERE name = 'OpenMetadata'
AND (JSON_EXTRACT(json, '$.connection.config.elasticsSearch') IS NOT NULL
OR JSON_EXTRACT(json, '$.connection.config.securityConfig') IS NOT NULL);

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 migration should also scrub the version history of this service. Previous versions are stored in entity_extension with the full connection, so they still carry elasticsSearch and securityConfig. Could you apply the same removal there, in both the MySQL and Postgres scripts?

-- MySQL
UPDATE entity_extension
SET json = JSON_REMOVE(json, '$.connection.config.elasticsSearch', '$.connection.config.securityConfig')
WHERE id = (SELECT id FROM metadata_service_entity WHERE name = 'OpenMetadata')
  AND extension LIKE 'metadataService.version.%';

-- Postgres
UPDATE entity_extension
SET json = (json #- '{connection,config,elasticsSearch}') #- '{connection,config,securityConfig}'
WHERE id = (SELECT id FROM metadata_service_entity WHERE name = 'OpenMetadata')
  AND extension LIKE 'metadataService.version.%';

I checked this locally: after an update, the history row keeps both fields once the current migration has run, and the statement above clears them. For the 2.0 backport, this needs to go in a 2.0.7 folder.

@mohityadav766
mohityadav766 force-pushed the security/ghsa-46f4-metadata-secret-mask branch from 6467045 to 0dd6e18 Compare October 26, 2026 09:51
@mohityadav766 mohityadav766 changed the title fix(security): restrict service connection visibility to bots and admins fix(security): restrict service connection reads to bots and editors Oct 26, 2026
@mohityadav766
mohityadav766 force-pushed the security/ghsa-46f4-metadata-secret-mask branch from 0dd6e18 to 7c2a161 Compare October 26, 2026 09:58
Return a service connection in full only to bots, masked to callers who can edit the
service (admins and explicitly delegated operators with EDIT_ALL), and not at all to
anyone else. The built-in OpenMetadata service no longer stores the Elasticsearch sink
or the ingestion-bot JWT (ingestion builds its own securityConfig on the fly and
reindexing reads the search config from the application config); it keeps only
host/port/auth provider. The server-connection JWT is masked for non-bot readers, and a
migration drops those fields from existing metadata service rows and their version
history. The decision is recorded in docs/decisions.
@mohityadav766
mohityadav766 force-pushed the security/ghsa-46f4-metadata-secret-mask branch from 7c2a161 to 58bb2aa Compare October 26, 2026 10:17
@mohityadav766
mohityadav766 added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-26T12:49:25Z)

Blocked the queue: integration-tests-postgres-opensearch

@gitar-bot

gitar-bot Bot commented Oct 26, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 closed / 4 findings

🔴 High risk · Changing service-connection access and deleting stored connection fields could expose secrets to unauthorized users or remove credentials needed by existing workflows.

Restricts service connection visibility so bots receive full connections, editors with EDIT_ALL receive masked connections, and other callers receive none, and removes the Elasticsearch sink and ingestion-bot JWT from the built-in service with a migration for existing rows and history. Four findings were resolved, including non-admin editors now getting no connection, the visibility rule being recorded in docs/decisions, the per-service EDIT_ALL entity load, and the ADR's Data Stewards error. No issues remain open.

✅ 4 closed
✅ Bug: Non-admin service editors (EditAll/owners) now get no connection

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java:67-73
The new branch only gives a connection back when getSubjectContext(...).isAdmin() is true. Anyone else who has EditAll on the service, such as service owners, Data Stewards or custom-role operators, now gets connection: null from GET, list, create and patch. The UI still shows Edit Connection to anyone with canEditAll, and EditConnectionFormPage builds its patch with compare(serviceDetails, configData) from that GET response. With a null connection, the form opens empty. Saving sends a patch that adds all of /connection, so every field and secret the user doesn't type again is lost, and the Connection tab and test connection stop working for these users. This also cuts against the Accepted CVE-reporting record, which says "explicitly delegated operators are trusted to configure services, manage connection details". Fix: base the masked-vs-null choice on the caller's permission on the service (for example, edit permission on the entity, or a VIEW_CONNECTION-style operation) rather than isAdmin(). Then masked connections still reach delegated operators and only VIEW_BASIC-only readers get null.

✅ Quality: New service-connection visibility rule is not recorded in docs/decisions

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java:61-73 📄 openmetadata-service/src/main/java/org/openmetadata/service/secrets/masker/EntityMasker.java:26-32 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java:92-101
This PR sets up a REST response rule that the UI, SDKs and ingestion all depend on. In the description's words: "Service connections are returned in full only to bots, masked for admins, and omitted entirely for all other users." It also says "the OpenMetadata metadata service no longer stores the Elasticsearch sink or the ingestion-bot JWT". No record in docs/decisions/ states either rule. The nearest ones, 2026-10-20-mcp-responses-mask-stored-credentials and 2026-10-14-cve-reporting-covers-unauthenticated-access-only, are about MCP masking and reporting scope. Because the repo squash-merges, these rules exist only in the PR body and code comments. The record would say: "A service GET returns the decrypted connection only to bots, a masked connection to <who: admins / callers with edit permission>, and no connection to anyone else; the built-in OpenMetadata service stores no securityConfig or elasticsSearch." Writing it down also settles the admin-only vs delegated-operator question raised in the other finding.

✅ Performance: Per-service EDIT_ALL check adds an entity load to every listed service

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java:69 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java:78-88 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java:103-106
decryptOrNullify(ResultList) runs canEditConnection once for each service. Every call builds a new getResourceContextById(service.getId()), and resolving it calls entityRepository.get with the owners/domains/reviewers/certification authorization fields, then evaluates the policies. So a list page of N services (limit goes up to 1,000,000) now does N extra entity and relationship loads plus N policy evaluations, and listVersions does one per version of the same service. This applies to admins too: they used to be resolved with a single isAdmin() check and now go through the full authorize path.

Fix: check getSubjectContext(securityContext).isAdmin() first, since admins always hold EDIT_ALL. For everyone else, authorize against a ResourceContext built from the service already in hand (the pre-resolved constructor, with ResourceContext.authorizationFields loaded during the list fetch), or cache the decision per service id within the request.

✅ Quality: ADR wrongly says Data Stewards hold EDIT_ALL and see masked connections

📄 docs/decisions/2026-10-26-service-connection-visibility-follows-edit-permission.md:24-25
The new record says callers holding EDIT_ALL include "admins, service owners, Data Stewards, and custom edit roles". The seeded DataStewardPolicy.json grants only ViewAll, EditDescription, EditDisplayName, EditLineage, EditOwners, EditTags, EditTier, EditGlossaryTerms, EditCertification and Trigger. It does not grant EditAll, so with this code Data Stewards get connection: null. Readers and the UI team will use this record as the visibility contract. Remove Data Stewards from the list, or say that only roles granting EditAll qualify.

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

@mohityadav766
mohityadav766 added this pull request to the merge queue Oct 26, 2026
@mohityadav766 mohityadav766 added the To release Will cherry-pick this PR into the release branch label Oct 26, 2026
@sonarqubecloud

Copy link
Copy Markdown

Merged via the queue into main with commit 4934556 Oct 26, 2026
140 of 143 checks passed
@mohityadav766
mohityadav766 deleted the security/ghsa-46f4-metadata-secret-mask branch October 26, 2026 14:12
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Backport to 2.0 is under way. It is built from openmetadata-collate, which holds the credentials needed to open a PR here, and it will comment again with the result — nothing is needed from you unless it reports otherwise.

@gh-bot-collate

Copy link
Copy Markdown

🤖 Backport to 2.0 opened as #36265. Claude resolved a cherry-pick conflict to build it. It merges itself once the checks pass, so read the diff now if this change is a sensitive one.

mohityadav766 added a commit that referenced this pull request Oct 26, 2026
…36219) (#36266)

* fix(security): restrict service connection reads to bots and editors

Return a service connection in full only to bots, masked to callers who can edit the
service (admins and explicitly delegated operators with EDIT_ALL), and not at all to
anyone else. The built-in OpenMetadata service no longer stores the Elasticsearch sink
or the ingestion-bot JWT (ingestion builds its own securityConfig on the fly and
reindexing reads the search config from the application config); it keeps only
host/port/auth provider. The server-connection JWT is masked for non-bot readers, and a
migration drops those fields from existing metadata service rows and their version
history. The decision is recorded in docs/decisions.

* test: align messaging connection visibility with edit permissions

(cherry picked from commit 4934556)
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.

2 participants