Skip to content

fix(data-products): guard ports against non-data-asset entities and harden portsView#30186

Open
sonika-shah wants to merge 1 commit into
open-metadata:mainfrom
sonika-shah:fix-data-product-ports-non-asset-guard
Open

fix(data-products): guard ports against non-data-asset entities and harden portsView#30186
sonika-shah wants to merge 1 commit into
open-metadata:mainfrom
sonika-shah:fix-data-product-ports-non-asset-guard

Conversation

@sonika-shah

@sonika-shah sonika-shah commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Problem

Opening a Data Product detail page can fail with a 500 and the toast:

Entity repository for tableColumn not found. Is the ENTITY_TYPE_MAP initialized?

The failing request is GET /api/v1/dataProducts/name/{fqn}/portsView.

Root cause

DataProductRepository.getPaginatedPorts loads each input/output port by grouping the
port relationship rows on their stored entity type and calling Entity.getEntities(...)
per type. Entity.getEntities resolves a repository via Entity.getEntityRepository,
which throws EntityNotFoundException for any type that has no registered repository —
including tableColumn, a search-index-only pseudo type (Entity.TABLE_COLUMN) that is
deliberately not a first-class entity.

So a single port relationship row pointing at a non-entity type makes the entire
portsView response 500, breaking the page for every port on that data product.

A port is meant to reference a data asset (a dataset such as a table, topic, or
dashboard). The bulk add-ports operation also did not validate this, so a resolvable but
non-asset entity (user, team, …) could be attached as a port.

Fix

  1. Harden the read path. getPaginatedPorts now skips relationship rows whose entity
    type has no repository (logged at WARN) instead of letting one row throw and fail the
    whole response. This is the same graceful-skip behaviour EntityUtil.populateEntityReferences
    already uses for orphaned references.

  2. Validate on add. Adding a port now rejects any reference that is not a real entity
    supporting the dataProducts field, reported as a per-row failure (consistent with the
    existing opposite-port and ownership guards). Eligibility is derived from the entity's
    own capabilities:

    Entity.hasEntityRepository(type)                       // a real entity (excludes tableColumn)
        && Entity.entityHasField(type, FIELD_DATA_PRODUCTS) // can belong to a data product

    There is no hardcoded type list, so new asset types are covered automatically as their
    schemas gain the dataProducts field.

Tests

Adds DataProductResourceIT#test_addPort_rejectsNonDataAssetEntity: a user (a real entity
that is not a data asset) is rejected when added as an input port, and the ports view still
loads with zero ports. Fails without change (2).

Note

Change (2) prevents new invalid rows via the public API. Change (1) is what recovers data
products that already have an invalid port relationship (created by non-API/historical
paths); such existing rows should also be cleaned up out of band.

Greptile Summary

This PR validates data-product ports and makes the ports view tolerate unregistered entity types. The main changes are:

  • Reject resolvable entities that cannot belong to a data product.
  • Skip unregistered relationship types while loading ports.
  • Add an integration test for rejecting a user as an input port.

Confidence Score: 4/5

The unresolved-input and mixed-row pagination paths need fixes before merging.

  • Unresolved references can disappear before per-row validation.
  • Read-time filtering can return short pages and incorrect totals while valid ports remain.

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java Adds port eligibility checks and graceful read-time skipping, but unresolved writes and mixed historical rows still produce incorrect bulk and pagination results.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java Adds integration coverage for rejecting a resolvable non-asset user, but not unresolved input or mixed valid and invalid pagination.

Reviews (1): Last reviewed commit: "fix(data-products): guard ports against ..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used (3)

  • Context used - CLAUDE.md (source)
  • Context used - openmetadata-ui-core-components/CLAUDE.md (source)
  • Context used - AGENTS.md (source)

…arden portsView

A data product port must reference a data asset (a dataset such as a table,
topic, or dashboard). Two gaps allowed and then surfaced invalid state:

- getPaginatedPorts fetched each port relationship by grouping on the stored
  entity type and calling Entity.getEntities, which throws
  EntityNotFoundException for a type that has no repository (e.g. the
  search-only pseudo-type "tableColumn"). A single such relationship row made
  the whole GET /dataProducts/{..}/portsView endpoint return 500.

- The bulk add-ports operation did not validate that a reference is a data
  asset, so resolvable non-asset entities (user, team, ...) could be attached
  as ports.

Fix:
- portsView now skips relationship rows whose entity type has no repository
  (logged at WARN), so a legacy/invalid row no longer breaks the response.
- Add-ports now rejects any reference that is not a real entity supporting the
  dataProducts field, reported as a per-row failure. Eligibility is derived
  from the entity's own capabilities (hasEntityRepository + dataProducts
  field), so newly added asset types are covered automatically with no list to
  maintain.

Adds an integration test asserting a non-data-asset entity is rejected as a
port and that the ports view still loads.
Copilot AI review requested due to automatic review settings July 17, 2026 13:48
@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.

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 backend safe to test Add this label to run secure Github workflows on PRs labels Jul 17, 2026
result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1);

if (isAdd) {
if (!isPortEligibleType(ref.getType())) {

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.

P1 Unresolved Inputs Bypass Validation

populateEntityReferences(assets) removes unresolved references before this loop. A tableColumn or unknown-type request therefore never reaches this guard, and the bulk response can omit the requested row instead of reporting the promised per-row failure.

Context Used: CLAUDE.md (source)

Comment on lines +664 to +670
if (!Entity.hasEntityRepository(entityType)) {
LOG.warn(
"Skipping {} port(s) of unresolvable type '{}' for data product {}",
entry.getValue().size(),
entityType,
dataProductId);
continue;

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.

P1 Filtering Breaks Port Pagination

When a page contains a historical unresolvable relationship, the DAO has already applied offset and limit before this branch drops the row. The response can return a short or empty non-final page while paging.total still counts the dropped row, so clients that stop on a short page can miss valid ports at later offsets.

Context Used: CLAUDE.md (source)

// registered entity (excludes search-only pseudo-types like tableColumn) that can itself
// belong to a data product (i.e. its schema declares a `dataProducts` field). This is derived
// from the entity's own capabilities, so it stays correct as new asset types are added.
private static boolean isPortEligibleType(String entityType) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: isPortEligibleType can throw for time-series entity types

hasEntityRepository returns true for time-series types (it also checks ENTITY_TS_REPOSITORY_MAP), but entityHasField resolves via Entity.getEntityRepository, which only consults ENTITY_REPOSITORY_MAP and throws EntityNotFoundException for TS-only types. So if a time-series entity reference is added as a port, isPortEligibleType throws instead of returning a clean per-row failure, re-introducing the 500 this PR aims to eliminate. The read-path guard in getPaginatedPorts has the same gap: for a TS type hasEntityRepository is true, so it proceeds to Entity.getEntities, which can throw. The primary tableColumn case is unaffected (it is in neither map). Guard the eligibility/read check against types not in the standard entity repository map, or catch the lookup failure.

Exclude time-series entity types so entityHasField (which throws for TS-only types) is never called on them.:

private static boolean isPortEligibleType(String entityType) {
  if (!Entity.hasEntityRepository(entityType) || Entity.isTimeSeriesEntity(entityType)) {
    return false;
  }
  return Entity.entityHasField(entityType, Entity.FIELD_DATA_PRODUCTS);
}
  • 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 Jul 17, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Hardens the data product ports read path and adds validation to prevent non-data asset entities from being assigned as ports. Ensure isPortEligibleType is updated to safely handle time-series entity types, as entityHasField may currently throw for those cases.

💡 Edge Case: isPortEligibleType can throw for time-series entity types

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java:629-643

hasEntityRepository returns true for time-series types (it also checks ENTITY_TS_REPOSITORY_MAP), but entityHasField resolves via Entity.getEntityRepository, which only consults ENTITY_REPOSITORY_MAP and throws EntityNotFoundException for TS-only types. So if a time-series entity reference is added as a port, isPortEligibleType throws instead of returning a clean per-row failure, re-introducing the 500 this PR aims to eliminate. The read-path guard in getPaginatedPorts has the same gap: for a TS type hasEntityRepository is true, so it proceeds to Entity.getEntities, which can throw. The primary tableColumn case is unaffected (it is in neither map). Guard the eligibility/read check against types not in the standard entity repository map, or catch the lookup failure.

Exclude time-series entity types so entityHasField (which throws for TS-only types) is never called on them.
private static boolean isPortEligibleType(String entityType) {
  if (!Entity.hasEntityRepository(entityType) || Entity.isTimeSeriesEntity(entityType)) {
    return false;
  }
  return Entity.entityHasField(entityType, Entity.FIELD_DATA_PRODUCTS);
}
🤖 Prompt for agents
Code Review: Hardens the data product ports read path and adds validation to prevent non-data asset entities from being assigned as ports. Ensure `isPortEligibleType` is updated to safely handle time-series entity types, as `entityHasField` may currently throw for those cases.

1. 💡 Edge Case: isPortEligibleType can throw for time-series entity types
   Files: openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java:629-643

   `hasEntityRepository` returns true for time-series types (it also checks `ENTITY_TS_REPOSITORY_MAP`), but `entityHasField` resolves via `Entity.getEntityRepository`, which only consults `ENTITY_REPOSITORY_MAP` and throws `EntityNotFoundException` for TS-only types. So if a time-series entity reference is added as a port, `isPortEligibleType` throws instead of returning a clean per-row failure, re-introducing the 500 this PR aims to eliminate. The read-path guard in `getPaginatedPorts` has the same gap: for a TS type `hasEntityRepository` is true, so it proceeds to `Entity.getEntities`, which can throw. The primary `tableColumn` case is unaffected (it is in neither map). Guard the eligibility/read check against types not in the standard entity repository map, or catch the lookup failure.

   Fix (Exclude time-series entity types so entityHasField (which throws for TS-only types) is never called on them.):
   private static boolean isPortEligibleType(String entityType) {
     if (!Entity.hasEntityRepository(entityType) || Entity.isTimeSeriesEntity(entityType)) {
       return false;
     }
     return Entity.entityHasField(entityType, Entity.FIELD_DATA_PRODUCTS);
   }

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 👍 / 👎 | Gitar

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (36 flaky)

✅ 4532 passed · ❌ 0 failed · 🟡 36 flaky · ⏭️ 95 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 436 0 4 16
✅ Shard 2 11 0 0 0
🟡 Shard 3 818 0 14 8
🟡 Shard 4 818 0 3 18
🟡 Shard 5 836 0 4 5
🟡 Shard 6 787 0 1 46
🟡 Shard 7 826 0 10 2
🟡 36 flaky test(s) (passed on retry)
  • Features/Pagination.spec.ts › should test pagination on Service version page (shard 1, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: metric (shard 1, 1 retry)
  • Pages/SearchSettings.spec.ts › Latest preview config wins when a superseded request resolves late (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a fully denied user sees neither asset type when browsing (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Database service (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › CSV with quoted FQN loads correctly in import grid (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › Column with dot in name under service with dot (shard 3, 1 retry)
  • Features/ContextCenterArchive.spec.ts › full document lifecycle: folder expand icon, upload, delete, restore, and permanent delete (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article listing search filters, clears, and shows empty state (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article list cards, recently viewed widget, and pagination work (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article edit persistence and unsaved title behavior are correct (shard 3, 2 retries)
  • Features/ContextCenterArticles.spec.ts › description: switching articles does not bleed unsaved content into next article (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › clearing search restores the unfiltered list (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › editing title updates the memory and the row reflects the new title (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › adding a linked asset in edit mode shows entity badge on the row (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › Shared memory shows the shared-with-specific-people description (shard 3, 1 retry)
  • Features/Glossary/GlossaryAdvancedOperations.spec.ts › should change domain on glossary (shard 3, 1 retry)
  • Features/Glossary/GlossaryCRUDOperations.spec.ts › should create glossary with mutually exclusive enabled (shard 3, 1 retry)
  • Features/Glossary/GlossaryTermRelationsGraphNested.spec.ts › viewing a child term: parent appears as a 1-hop neighbour via parentOf edge (shard 4, 1 retry)
  • Features/SearchExport.spec.ts › Export queues a background job and downloads from the jobs tray (shard 4, 1 retry)
  • Features/Table.spec.ts › should persist page size (shard 4, 1 retry)
  • Flow/ApiServiceRest.spec.ts › add update and delete api service type REST (shard 5, 1 retry)
  • Flow/ObservabilityAlerts.spec.ts › Ingestion Pipeline alert (shard 5, 1 retry)
  • Pages/CustomProperties.spec.ts › Date (shard 5, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate Domain Rule Is Not (shard 5, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 6, 1 retry)
  • Pages/Glossary.spec.ts › Drag and Drop Glossary Term (shard 7, 1 retry)
  • Pages/Glossary.spec.ts › Create term with related terms, tags and owners during creation (shard 7, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Check for Circular Reference in Glossary Import (shard 7, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Import partial success - some terms pass, some fail (shard 7, 1 retry)
  • ... and 6 more

📦 Download artifacts

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants