Skip to content

Fixes #34689: record every Assets tab edit on the asset as its own PATCH - #35026

Merged
manerow merged 24 commits into
mainfrom
fix/1526-assets-tab-edits-as-patches
Oct 16, 2026
Merged

manerow merged 24 commits into
mainfrom
fix/1526-assets-tab-edits-as-patches

Conversation

@manerow

@manerow manerow commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #34689

Problem

Adding or removing a tag, glossary term, domain or data product from an Assets tab changed the assets but recorded nothing on them: no new version, no "updated by", no change event for the activity feed, alerts or audit log. The tab endpoints wrote relationship rows directly instead of saving the asset.

PR #34690 did this through a second save pipeline. This PR starts over so an entity has one way to be saved.

What this PR changes

An Assets tab edit is now exactly the PATCH the user could make on each asset's page. EntityPatchBatch takes the selected assets in groups of 100 and loads each group in one query. For each asset it applies the edit to a copy, turns the difference into a JSON patch, authorizes the asset as that PATCH, and saves it through the EntityRepository.patch code a single PATCH runs, with the same rules and versioning. A selected column is saved through its table. An asset an approval workflow gates is held as that PATCH would be, and reported as pending with its change request. The tag tab's background job keeps the request's impersonation, so isImpersonated() rules apply there too.

Two related fixes:

  • A PATCH now sends the entity to search after its inherited fields are filled, so removing an asset's own domain shows the inherited one in search at once.
  • A table's classification tags and data products are no longer copied onto its column search entries. Its glossary terms still reach them as inherited terms, and test cases still inherit table tags.

The principles are recorded in docs/decisions/.

API change

The tag and glossary term assets/add and assets/remove endpoints no longer answer 403 for the whole request. An asset the caller may not edit comes back as a failed row with the reason, in a 200 whose status is failure or partialSuccess (for tags, in the job's outcome). Clients must read status, not only the HTTP code. Domain and data product endpoints still answer 403 to a caller who may not edit that domain or data product.

What users will notice

  • Every Assets tab edit creates a version, sets "updated by", and shows up in the activity feed and alerts.
  • An asset the user may not edit is reported with its reason, and the rest goes through.
  • Removing a label from a schema or service keeps it on assets inside that carry it themselves.

How it was tested

New AssetsTabVersioningIT (versions, events, columns, partial failure, 251 assets, parity with the PATCH), AssetsTabAuthorizationIT (owner-only and deny rules, a domain editor, an impersonating bot) and AssetsTabSearchEntriesIT, plus Jest tests for refused removals. With the related suites, approval holds included, 1,218 tests pass on MySQL; the most affected also on Postgres.

Median of 5 rounds: a glossary term on 100 tables takes 2.7 s (3.4 s on main).

Not in this PR

Sending each group's search writes as one bulk request (#35023).

RetriggerView in GreptileConfidence Score: 4/5

The cache must have an explicit size cap before merging to satisfy the repository’s requirement.

Findings

  1. P2 Job outcomes grow without bounds ▶

Summary

Assets tab edits now use each asset’s normal PATCH path.

  • Assets tab edits save each selected asset through its normal PATCH path.
  • Bulk edits report each asset the caller cannot edit.
  • Search entries now reflect inherited fields without copying every table label to columns.

Acknowledged limitations:

  • Concurrent overwrites without a version check are explicitly documented as a known limitation.
  • manerow explicitly deferred search conflict retries and grouped search writes to separate work.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Assets tab edit] --> B[Resolve selected assets and columns]
  B --> C[Group by saved entity]
  C --> D[Load up to 100 entities]
  D --> E[Apply edit to a copy and build PATCH]
  E --> F[Check permission for each asset]
  F -->|Refused| G[Failed row]
  F -->|Allowed| H[Normal entity PATCH]
  H -->|Approval required| I[Pending change request]
  H -->|Saved| J[Version, search, and change event]
  G --> K[Bulk result]
  I --> K
  J --> K
Loading

Reviews (1) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile

@manerow
manerow requested a review from a team as a code owner October 9, 2026 12:40
@manerow manerow added safe to test Add this label to run secure Github workflows on PRs backend labels Oct 9, 2026
@manerow manerow self-assigned this Oct 9, 2026
authorizeBulkAssetsPermission(
securityContext, request.getAssets(), MetadataOperation.EDIT_TAGS);

PatchRequester requester = PatchRequester.fromRequest(securityContext, authorizer);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since each asset is now authorized inside the background job, impersonation-based policy conditions stop working on the tag endpoints. isImpersonated() and impersonatedBy(...) read only the ImpersonationContext thread-local (RuleEvaluator.java:614, :634), which is set by JwtFilter on the request thread and isn't copied into the job (RequestLatencyContext.wrapWithContext only carries the latency context).

So a Deny rule like isImpersonated() && matchAnyTag('Sensitivity.Confidential') evaluates to false here, and a bot acting for a user (e.g. an MCP agent) could tag or untag assets that rule is meant to protect. On main the check ran on the request thread, so it applied. Glossary, domain and data product are synchronous, so they're fine.

Setting ImpersonationContext from requester.actor().impersonatedBy() at the start of the job (and clearing it after) should fix it. A test with an impersonated request against such a deny rule would be good to have.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, thanks. The tag tab's job ran without the request's impersonation, so isImpersonated() and impersonatedBy(...) rules were skipped there. d0d9895 sets ImpersonationContext from requester.actor().impersonatedBy() when the job starts and clears it when it ends.

New IT AssetsTabAuthorizationIT.aBotActingForAUser_isRefusedWhatAnImpersonationRuleDenies: a bot impersonating a user adds a tag to two tables, and the user has a deny rule isImpersonated() && matchAnyTag(...) covering one of them. Without the fix the protected table got the tag; with it, only the other table changes.

.toList();
return Response.ok()
.entity(
repository.bulkRemoveGlossaryToAssets(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A refused removal now fails silently in the UI. Before, a user without permission got a 403 for the whole request and the UI showed an error. Now it's a 200 with failed rows; the add dialog shows those, but the remove handler in AssetsTabs.component.tsx (onAssetRemove) only awaits the call and never looks at the result. So the dialog closes as if it worked, the asset stays, and no error appears. Same for rule failures, and on all four tabs.

Could we handle the BulkOperationResult on remove the same way the add dialog does?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, that was a regression. 61e5d64 and c3989f9 make the remove path read the result the way the add dialog does, on all four tabs. For the synchronous tabs it reads the BulkOperationResult. For the tag tab, whose removal runs as a job, it waits up to 2 minutes for the job's outcome on the websocket. When an asset is refused, an error toast shows the reason. If the outcome doesn't arrive in time, or the socket is disconnected, it asks the user to refresh the page to see the result. Tests are in AssetsTabs.utils.test.ts.

return Response.ok().entity(repository.bulkAddAndValidateGlossaryToAssets(id, request)).build();
return Response.ok()
.entity(
repository.bulkAddAndValidateGlossaryToAssets(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This changes the API response for a user who isn't allowed: before it was a 403 for the whole request, now it's a 200 with status: FAILURE and a failed row (the updated GlossaryTermResourceIT cases assert exactly that). Scripts, SDK users or integrations that only check the HTTP status will now read a refusal as success.

It makes sense with per-asset authorization, but since it's a behaviour change for API clients, could we call it out in the PR description and release notes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The description now has an "API change" section. It says the tag and glossary term endpoints answer 200, with status failure or partialSuccess and failed rows, where they used to answer 403, so clients must read status. I also added the Backward-Incompatible-Change label so it gets into the 2.1 release notes.

@manerow
manerow added this pull request to the merge queue Oct 15, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 15, 2026
@github-actions github-actions Bot added the merge conflict Conflicts with the latest main; removed from the merge queue label Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — merge_conflict (2026-10-15T13:20:54Z)

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

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

…dits-as-patches

# Conflicts:
#	openmetadata-service/src/main/java/org/openmetadata/service/events/ChangeEventHandler.java
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ColumnRepository.java
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java
@github-actions github-actions Bot removed the merge conflict Conflicts with the latest main; removed from the merge queue label Oct 15, 2026
@manerow
manerow enabled auto-merge October 15, 2026 14:17
@manerow
manerow added this pull request to the merge queue Oct 15, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 15, 2026
@github-actions github-actions Bot added the merge conflict Conflicts with the latest main; removed from the merge queue label Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — merge_conflict (2026-10-15T23:33:19Z)

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

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

…dits-as-patches

# Conflicts:
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java
#	openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java
#	openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
#	openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java
@github-actions github-actions Bot removed the merge conflict Conflicts with the latest main; removed from the merge queue label Oct 16, 2026
@manerow
manerow enabled auto-merge October 16, 2026 07:15
@gitar-bot

gitar-bot Bot commented Oct 16, 2026

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

🔴 High risk · Changing tag/glossary bulk endpoints from whole-request 403s to per-asset 200 failures may mislead clients that rely on HTTP status alone.

Assets tab tag, glossary term, domain and data product edits now save each asset through the same PATCH path as the asset page, so every edit creates a version, sets "updated by", and emits change events. Resolved that one unresolvable asset no longer fails every asset of its type, and that tag-tab job refusals are now reported instead of going unrecorded after a 120 s wait. No open issues found.

✅ 2 closed
✅ Bug: One unresolvable asset fails every asset of its type, including saved ones

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/AssetEditService.java:136-150 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityPatchBatch.java:83-97
EntityUtil.populateEntityReferences skips a reference given only by FQN when that FQN doesn't resolve, and leaves its id null. resolve() still builds a Selection with entityId == null. When EntityPatchBatch.load reaches that group, findEntitiesByIds calls UUID::toString on the null id and throws an NPE. The NPE leaves EntityPatchBatch.apply, and AssetEditService.editType catches it and runs failAll over every selection of that entity type. The same catch-all handles any group-level error, such as a failed DB load. Example: 250 tables are selected and the bad reference lands in the third group of 100. The first two groups (200 tables) are already committed with new versions and events, yet the response lists them as failed, and recordBulkAssetsChange leaves them out of the domain or data product's change event. Fix: reject a reference with no id in resolve() so it fails as its own row, and handle errors per group inside applyGroup so a group failure marks only that group's ids as failed.

✅ Quality: Unrecorded 120 s wait on tag-tab job after which refusals go unreported

📄 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.utils.ts:30 📄 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.utils.ts:81-95 📄 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.utils.ts:109-123
BULK_ASSETS_JOB_WAIT_MS = 120000 sets how long a tag Assets tab removal keeps the spinner up waiting for the job's outcome. If the job takes longer, or the socket isn't connected, outcomeOf resolves undefined and removeAndReportRefusals shows no toast. So a refused asset looks removed, which is the problem this commit is meant to fix. The 120 s figure is a judgment call, and nothing in docs/decisions/ records it (the bulk-edit records cover authorization and composition, not a client wait budget).

The record would state: "A tag Assets tab removal waits up to 120 s for its job outcome on the bulk assets channel. After that, or with no socket connection, it stops waiting and reports nothing." Either add that record, or decide something else on timeout, for example show a toast saying the outcome is still pending. You could also reuse the existing TWO_MINUTE_IN_MILLISECOND constant.

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

socket?: Socket,
waitMs = TWO_MINUTE_IN_MILLISECOND
) => {
const finished = new Map<string, BulkAssetsJobOutcome>();

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 Job outcomes grow without bounds

finished caches every completed job received on the shared channel, including unrelated jobs, without a size limit. It keeps collecting while the removal waits for its response or job outcome. The repository requires every cache to have an explicit size cap, so this must be bounded before merging. Cap the stored entries and discard them when consumed; the timeout does not limit how many outcomes the cache can hold.

Context Used: CLAUDE.md (source)

@sonarqubecloud

Copy link
Copy Markdown

@manerow
manerow added this pull request to the merge queue Oct 16, 2026
@sonarqubecloud

Copy link
Copy Markdown

Merged via the queue into main with commit 8e88d52 Oct 16, 2026
86 of 88 checks passed
@manerow
manerow deleted the fix/1526-assets-tab-edits-as-patches branch October 16, 2026 10:53
shah-harshit pushed a commit that referenced this pull request Oct 16, 2026
…TCH (#35026)

* feat(entities): apply a bulk edit as one authorized PATCH per entity

* fix(entities): index a patched entity with the fields it inherits

* fix(assets): save every Assets tab edit as the asset's own PATCH

* fix(search): keep a table's tags and data products off its column entries

* docs(decisions): record how bulk edits write, authorize and index

* fix(entities): fail only the group that cannot be loaded or recorded

* fix(assets): refuse an asset whose labels clash with its glossary's tags on the glossary term tab

* refactor(assets): derive the Assets tab status with the shared bulk status rule

* test(assets): drop the fail-before-pass gate test that per-asset saves replace

* test(assets): compare tab and PATCH labels without their appliedAt

* test(assets): keep the search entry checks clear of the response cache and of unordered updates

* test(assets): check the column entries of all tables in one search per poll

* test(assets): drop the domain move check that waits on main's lossy column propagation

* fix(assets): authorize the tag tab job with the request's impersonation

* fix(ui): report assets the server refuses to remove from an Assets tab

* chore(playwright): auto-refresh impact-map.generated.json

* fix(ui): ask to refresh when a tag tab removal's outcome does not arrive

* test(glossary): expect a refused bulk asset row instead of a 403

* fix(search): send column entries the glossary terms they inherit from their table

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Vamsi-klu pushed a commit to Vamsi-klu/OpenMetadata that referenced this pull request Oct 17, 2026
…ts tile padding (open-metadata#35609)

* Fix dark-mode status counts, tab counts, table header divider and entity icons

- Ingestion/automation status counts use core Badge pills, left-aligned
- Active tab counts use an outlined brand Badge instead of a solid fill
- Core Table header divider uses border-primary in dark so it stays visible
- FeaturedIcon light theme keeps its tinted tile in dark instead of going solid
- Avatar gains placeholderIconColor; entity avatars use the brand tint by default

* Pin the Connections composer to the canvas edge and record the tinted-icon decision

The Connections list footer sat inside the padded PageLayout.Content, so the
AI composer stopped 8px above the canvas edge; it now mounts on the page root
like Home's footer slot.

* Mark Classic mode as deprecated in the mode switchers and the admin setting

Classic shows a Deprecated badge with a retirement notice and release-notes
link only where a user changes mode (AI-shell switcher, classic profile
dropdown) and on the admin Default App Mode setting.

* Give Insights tab tiles and DQ pie cards consistent core Card padding

Total Data Assets list, Platform Insights KPI cards and the Data Quality pie
widgets render their content in Card.Content instead of caller padding
(tw:p-3, a gray p-4 box, LESS padding on the DQ dashboard), and the embedded
Insights sections drop their surface slab and extra header padding.

* Move the status badge color map to IngestionStatusCount.constants.ts

* Keep domain and data product avatars solid in both themes

Users pick these colors, so design keeps the solid fill until custom colors
are reworked. Reverts getEntityAvatarProps and drops the now-unused
Avatar.placeholderIconColor; the decision record now covers FeaturedIcon only.

* Drop the darker bands from the column detail drawer in dark mode

The title block, content wrappers and footer painted @grey-9/@grey-50, which
resolve to bg-secondary (darker than the drawer's overlay surface) in dark;
they are transparent there now. Key profile metric tiles use an outline instead
of the darker fill in dark, and the custom properties list uses bg-surface
instead of the page-colour bg-primary. Light mode is unchanged.

* Render the contract result pill as a core Badge and fix the Beta tab chip overlap

The header Contract Failed / Aborted / Running pill was a secondary Button
recolored with ! classes; its 600 text measured 2.7:1 in dark. It is now a
shared ContractLatestResultBadge (core Badge palette, 4.7:1 dark / 6.1:1
light) that links to the Contract tab, on both data assets and data products.

Core Badge: a linked badge no longer adds tw:text-inherit, which won the
tailwind-merge conflict and dropped the badge's own text color.

TabsLabel: the Beta chip is no longer absolutely positioned over the next tab.

* chore(playwright): auto-refresh impact-map.generated.json

* Keep the empty test-status chart inside its card

The no-data footer sits in a core Tooltip whose trigger is w-max, so the
placeholder SVG resolved its 100% width against max-content (313px) and
spilled over the neighbouring cards. The trigger now takes the card width.
Also tidies imports left by the contract badge change.

* Address review: keep HoverCard open on focus, keep DP contract link on version

HoverCard: focus moving from the trigger into the card no longer schedules a
close, so a link inside it (the Classic deprecation release-notes link) stays
usable from the keyboard.

Data product header: the contract result badge keeps the selected version in
version history, as the previous handleTabChange navigation did.

* fix(ui): converge Archive badge count after an async hard delete (#35446)

* fix(ui): converge Archive badge count after an async hard delete

A drive file's hard delete answers 202 Accepted and removes the row on a
background thread. ContextCenterArchivePage invalidated the archive-count
query the moment that request resolved, so the refetch raced the background
deletion. When the refetch won, React Query cached the pre-delete total and
nothing invalidated the key again, leaving the sidebar badge one too high for
the rest of the session.

Replace the single fire-and-forget invalidation with reconcileCountAfterAsyncDelete:
drop the count optimistically (matching what the page already does to its own
list), then refetch until the server agrees or the attempts run out (5 x 1s).

Uses refetchQueries rather than invalidateQueries — invalidation only marks an
*inactive* query stale without fetching it, so the reconcile would silently
no-op whenever the sidebar observing the key is unmounted.

On timeout the corrected figure is written back: the last refetch leaves the
server's pre-delete total in the cache, and keeping that would restore the
staleness this fixes. Deletions still in flight are counted per key so a
concurrent delete derives its target from the server's number rather than a
cached value a sibling's refetch may have just overwritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ui): poll the deleted row's absence to converge the Archive badge

The reconcile targeted `baseline - started`, a count frozen when the first
delete started. Anything else moving the archive count inside the ~5s window
threw it off, and the page's own restore handler invalidates that very key: a
restore mid-window let the poll see its target early and exit, leaving the
badge one too high once the background delete landed — the bug this helper
exists to fix. A soft-delete in the window broke it the other way, with
`clampedTotal` capping the badge one too low.

Use the deleted entity's own absence as the completion signal instead: poll
`getContextFileById` until it stops resolving, then refetch once and keep
whatever the server reports. No precomputed target, so a concurrent restore or
soft-delete cannot skew it, and the baseline/in-flight bookkeeping and the
clamp all go away with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ui): probe the archive delete with Include.All and only treat 404 as landed

The absence probe called getContextFileById without an include param, but
GET /contextCenter/drive/files/{id} defaults to include=non-deleted. Every row
on the Archive page is already soft-deleted (the list is loaded with
include=deleted), so the probe 404'd before the background hard delete even
started. The rejection handler mapped any error to "landed", so the first poll
ended the wait: the race this fixes was still there, only pushed back ~1s.

Let getContextFileById take an include argument and pass Include.All, so the
probe resolves true only once the row is genuinely purged. Narrow the predicate
to a 404 as well, leaving a transient error to the next attempt rather than
ending the poll early.

The helper's unit tests stub hasLanded, so neither gap was visible to them:
cover the include wiring in a new assetAPI test and the transient-error case
in the helper's own suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* lint fix

* fix(ui): mark the archive reconcile float with void

Sonar flagged the unawaited reconcileCountAfterAsyncDelete call. The float is
deliberate — awaiting it would hold `isDeleting` through the ~5s poll and spin
the modal button — so `void` says so explicitly. Nothing escapes: the probe
handles both branches and refetchQueries resolves rather than throws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Fixes #35473: Export and Bulk Edit only the filtered test cases (#35474)

* Fixes #26756: Add filter and pagination support to test case export endpoints

GET /v1/dataQuality/testCases/name/{name}/export and /exportAsync now accept
testCaseStatus, testCaseType, entityFQN, entityLink, tags, tier, owner, limit
and offset through one shared @BeanParam, so the two endpoints cannot drift.

Matches are resolved through search with the same search_after walk the
filtered bundle-suite add uses, so the export selects exactly what the
filtered test case list shows (including table-inherited owners); rows are
read from the database. Async jobs carry the validated filters as
CsvAsyncJobArgs.TestCaseExportArgs, and progress reflects the filtered count.
Requests without filters or paging keep the original export path.

* Support every test case list filter on the test case export endpoints

The export now also accepts q, testPlatforms, dataQualityDimension,
serviceName, dataProductFqn and startTimestamp/endTimestamp, so a Bulk Edit
opened from a filtered test case list can pass the list's filters through
unchanged instead of silently exporting a superset.

* Harden filtered test case export coverage and test isolation

Validate every CSV row and cover inherited metadata, suite and platform scopes, entity links, status lists, and timestamp validation. Use an export caller per test to avoid concurrent result-retention conflicts, and record the domain-filtered paging contract.

* Document test case export filters as query parameters in OpenAPI

TestCaseExportQuery's private fields had no getters, so swagger could not
see them as properties and documented the @BeanParam as a JSON request body
instead of listing the filters as query parameters.

* Fixes #35473: Load only the filtered test cases into Bulk Edit

Bulk Edit always exported every test case in scope, whatever the list it
was opened from was filtered to. The test case list, the table Data Quality
tab and the test suite page now carry their active filters into the Bulk
Edit URL, and Bulk Edit sends them with the export that loads the grid.

The platform list builds its search request and its Bulk Edit filters
from the same helper, so the two cannot drift.

* Export only the test cases matching the list filters

The Export action in the test case manage menu exported everything in
scope. It now sends the same list filters as Bulk Edit, so both select the
test cases the list shows.

* Fix table test case filters after list actions

* Fixes open-metadata/openmetadata-collate#7447: forward aria-label on ProgressBarBase; show Deprecated on the Column Bulk Operations page (#35491)

* fix(ui): forward aria-label and aria-labelledby on ProgressBarBase

ProgressBarBase had a closed props interface and rendered none of the
ARIA naming props, so every caller's aria-label was dropped and the
progressbar had no accessible name. ProgressBar passes them through too.

* feat(ui): support a text badge on sub-nav items

SubNavItem gains badgeKey, an i18n key rendered as a text badge in place
of the count, so an item can be tagged (e.g. Deprecated). The badge text
joins the item's aria-label, which would otherwise hide it.

* feat(ui): mark Column Bulk Operations as Deprecated in the classic sidebar

Classic sidebar items gain isDeprecated, rendered as a warning badge.
Column Bulk Operations switches from the Beta badge to Deprecated.

* Revert "feat(ui): mark Column Bulk Operations as Deprecated in the classic sidebar"

This reverts commit 3be32e5e66747f6de11bea19ab4d003d95cf7621.

* Revert "feat(ui): support a text badge on sub-nav items"

This reverts commit 1b1e78bd1fd5937d01a263edb83bf9d44e574756.

* feat(ui): show a Deprecated badge on the Column Bulk Operations page header

Mark the feature deprecated on the page itself rather than in the
navigation: the badge sits next to the title in AI mode and next to the
breadcrumb in classic mode. The classic sidebar drops its Beta tag.

* fix(ui): align the Deprecated badge with the classic breadcrumb

The breadcrumb's bottom margin made the row taller than the text, so the
centred badge sat low. Move the margin from the breadcrumb to the row.

* feat(ui): explain the Column Bulk Operations deprecation on badge hover

Hovering (or keyboard-focusing) the Deprecated badge opens a HoverCard
stating the feature is deprecated but remains fully functional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): describe the deprecation to screen readers on the badge

The hover card is portalled and absent until opened, so the badge button
now points aria-describedby at a visually hidden copy of the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): use the core Tooltip for the Column Bulk Operations deprecation

The explanation is non-interactive, so core Tooltip fits better than
HoverCard: it wraps the badge in its own focusable trigger and links its
content via aria-describedby, removing the raw AriaButton and the
visually hidden duplicate description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): trim the deprecation Tooltip's class overrides to the minimum

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fixes #35519: Stabilise flaky glossary Playwright specs (#35520)

* Fixes #35519: Stabilise flaky glossary Playwright specs

- Related terms: pick the relation type through selectOptionWithRetry and
  re-pick until the trigger shows it; the bottom-row non-modal Select closed
  on scroll or took a stale click on the neighbouring option.
- addAssetToGlossaryTerm: wait for PUT /assets/add before returning so
  teardown cannot delete the term mid-add.
- Tag label row mapper: tolerate a tag_usage row whose tag/term is gone
  instead of throwing an NPE that made the asset unreadable and undeletable.
- Request tags: open the glossary by FQN so the /glossary auto-select
  redirect cannot replace the request-tags navigation.
- Glossary form tags: centre the trigger before opening so the drawer's
  late scroll does not close the dropdown.
- Glossary import: use saveBulkImport so the toast wait starts before Update.
- Bulk edit glossary: clean up on a fresh API context (idle keep-alive
  socket hang up).

* chore(playwright): auto-refresh impact-map.generated.json

* Drop positional locators from glossary related-terms helpers and spec

Rows are found by the id the add-row click introduces (ids are Date.now()),
the relation-type trigger by button[aria-haspopup=listbox], and the row to
remove by its trigger's selected value. Prune the now-stale suppressions.

* chore(playwright): auto-refresh impact-map.generated.json

* Trim review comments to one-line summaries

* Lower recorded no-positional-locator suppression count to 552

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(security): bump guava to 33.7.2-jre, hibernate-validator to 8.0.6.Final, java-jwt to 4.6.2 (#35291)

* fix(security): manage guava 33.7.2-jre in root pom for all modules

#35245 bumped guava only in openmetadata-service; common, spec, sdk and
java-client still resolved 33.4.8-jre (dropwizard-jersey) and
33.3.1-android (google-auth-library). CVE-2026-102554.

* fix(security): bump hibernate-validator to 8.0.6.Final

CVE-2026-93879 (SNYK-JAVA-ORGHIBERNATEVALIDATOR-20877468): EL injection via
unescaped message parameters. The fix (HV-2256, 870485e384 'Escape message
parameters by default') is contained in 8.0.6.Final; Snyk only lists
9.2.0.Beta1. 8.0.6.Final keeps jakarta.validation-api 3.0.2.

* fix(security): bump java-jwt to 4.6.2

CVE-2026-85473 / GHSA-cg7w-5cc2-f2xm (SNYK-JAVA-COMAUTH0-20937619): out-of-range
timestamp claims kill token-parsing threads; vulnerable [4.0.0,4.6.2).
Bumps the root java-jwt.version and the SDK's jwt.version, and points the
integration-tests literal at the shared property.

* Fixes open-metadata/openmetadata-collate#4719: let a password reset lift the login lockout (#35523)

* Fixes open-metadata/openmetadata-collate#4719: let a password reset lift the login lockout

A user who asks an admin for a password reset has usually locked the
account with failed logins first, and the reset did not lift the
lockout, so the new password was refused until the block expired.

Failed logins were counted by email, but the password change and reset
paths cleared them by user name and so never cleared anything. The
counters also live in each server's memory, so a reset served by one
server, or made with the ops CLI, could not reach the counter of the
server that answered the next login.

Count failed logins against the stored password hash instead. Every
login already reads that hash from the database, so a new password set
by any path starts with no failures on every server. A correct password
clears the failures counted against it, which keeps the MCP login
form's reset-on-success working now that failures are no longer keyed
by the bare email. The address-only checks that form uses resolve the
account first and follow the same rule. The rule is recorded in
docs/decisions/2026-10-15-basic-auth-failed-logins-count-against-the-stored-password.md.

LoginAttemptCache also wrote failures under the key as given while
reading it lowercased; it now lowercases both.

The sign-in page now shows the login servlet's error, which it reports
as `error` rather than `message`, so a lockout no longer reads as an
invalid password.

* chore(playwright): auto-refresh impact-map.generated.json

* chore(playwright): auto-refresh impact-map.generated.json

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fixes #33865: Inline lineage editing via node and column menus (#34551)

* feat(lineage): gate lineage editing on permission instead of edit mode

Add canEditLineage to the lineage store, computed from hasEditAccess,
deleted, platformView, entity type, and scene band instead of the
isEditMode toggle. Remove the Edit Lineage button from CustomControls;
gate the edge overlay buttons, node connectability, delete-key handler,
and drag-drop sidebar on canEditLineage/canEditScene. isEditMode stays
in the store but nothing sets it anymore.

* refactor(lineage): remove edit mode

* feat(lineage): confirm node and edge deletes

Add a node-delete confirmation dialog, route node removal (menu and
the temporary-node remove button) through it, and make Delete/Backspace
open the node or edge confirmation instead of deleting immediately.

* refactor(lineage): share edge creation between connect and add-lineage

* feat(lineage): add lineage popover

Standalone, props-driven AddLineagePopover: step 1 entity type (filtered to
TABLE and DASHBOARD_DATA_MODEL when opened from a column, since those are the
only search hit shapes getEntityChildrenAndLabel can flatten via its
getTableEntityChildren resolver), step 2 entity search excluding the current
node, step 3 column picker only when opened from a column. Picking the final
value submits immediately (no Add button); placement is left for upstream,
right for downstream, no arrow.

Task 6 will mount this in LineageMap.

* feat(lineage): node menu for adding upstream/downstream and deleting

Adds LineageNodeMenu (a per-node dots menu) and mounts it in CustomNodeV1
in place of the already-dead always-there remove button. Wires LineageMap
to open the shared AddLineagePopover from the menu's Edit Upstream/Edit
Downstream actions and route Delete through the existing node-delete
confirmation. Closes the popover on canvas pan/zoom.

* feat(lineage): column menu for adding column-level lineage

* fix(lineage): keep node menu clear of the drill button

SceneDrillButton is absolutely positioned over the same top-right corner
as the new node ⋮ menu, so it always painted on top and swallowed the
click. Shift the drill button left via a has-lineage-node-menu modifier
class whenever the menu renders, and give the menu trigger an explicit
24x24 size so the offset matches its real footprint.

* fix(lineage): anchor add-lineage popover to the menu trigger

Dropdown.DotsButton is a plain function component with no forwardRef, so
ref={triggerRef} placed on it never attached in React 18 and the popover
fell back to the viewport origin. Anchor the ref to the wrapper div both
LineageNodeMenu and LineageColumnMenu already own instead.

* refactor(lineage): remove drag-and-drop node creation

* fix(lineage): expose entity search testid on add-lineage popover

* feat(lineage): move edge actions into the edge drawer

Without edit mode an edge click opens the modal edge drawer, which hid the
floating edit/delete toolbar. Editors now get Edit Pipeline / Delete from a
menu in the drawer header; the canvas toolbar is removed.

Also fixes issues edit mode used to mask:
- edge clicks also reached the pane click handler and cleared the selection,
  so aggregated edges never opened the drawer
- a direct load of the Field band lost its column layer to the default layer
  setting, collapsing column lists and hiding metric column edges
- adding a column edge at the Field band always exhausted the scene refetch
  retries because the confirmation only matched entity edges

* test(lineage): drive lineage editing through node and column menus

* fix(lineage): address final review of inline lineage editing

- Delete key on a selected node matched a drawer copy by reference and never
  fired; match by entity id and only offer it for removable, non-root nodes
- Delete key on a selected edge ignored editability, letting rollup edges reach
  the delete confirmation; require an editable edge
- Dropdown.DotsButton now honours a caller's aria-label, so lineage node, column
  and edge-drawer menus announce their purpose instead of "Open menu"
- The add-lineage popover title names its dialog
- Replace suppressed hook deps in NodeChildren with the real dependencies
- Read-only lineage checks wait for rendered nodes before asserting no menu

* refactor(lineage): drop drag-to-connect node handles

Lineage is added from the node and column menus, so the connectable handles
on each node side are no longer needed. The expand/collapse buttons keep
their handle placement classes. Removes the unused drag-connect path and the
dead buildConnectPayload helper.

* fix(lineage): mark field-bearing assets expandable without field payloads

The asset band fetches no field payloads, so tables, topics, dashboards and
other field-bearing kinds were reported as non-expandable and semantic zoom
could not drill into them.

* feat(lineage): container node cards, control bar and zoom-to-node drill

- Container nodes (service/database/schema/domain/data product) render
  logo + name, a 'Type · kind' subtitle and a child count pill, at a
  fixed width by node type
- Control bar: mini-map toggle state, zoom level, fit to screen replaces
  fullscreen, aria-labels
- Semantic zoom drills only into the node under the pointer and shows a
  toast when that node has no deeper level
- Hover via ReactFlow node events, full-graph loading overlay, ReactFlow
  as the canvas root with pan-on-focus

* test(lineage): use fit to screen and pan helpers in lineage specs

* chore(playwright): auto-refresh impact-map.generated.json

* style(lineage): replace LESS variables with theme tokens for dark mode

- Lineage LESS files now use var(--om-*) tokens instead of variables.less,
  so node, column, status and panel colours follow the active theme
- Add theme-aware --om-color-fg-quaternary, utility-brand-100 and
  utility-indigo-100/600 tokens to keep light-mode values unchanged
- Floating canvas panels (rail, breadcrumbs, layers, skeleton, onboarding)
  use bg-surface / bg-overlay-surface instead of the page colour
- Replace raw tw palette classes in PortsLineageView
- Drop rules left dead by the removed edit mode and sidebar

* fix(lineage): keep the node menu clear of the scene drill button

The drill-button padding on .label-container pushed the ⋮ menu under the
absolutely positioned drill button on drillable editable nodes, so the
menu could not be clicked. Reserve the gap before the menu instead.
Also point the band-selection test at the layer button; the
.lineage-map-layer-control class no longer exists.

* refactor(lineage): migrate lineage components from AntD to core components

Replace antd/@ant-design usage in the lineage UI with
@openmetadata/ui-core-components:
- LineageConfigModal: core Modal + react-hook-form number inputs
- AddPipeLineModal: core Modal, Input, Button
- LineageSearchSelect: Select.ComboBox with load-more paging
- NodeChildren, CustomNode.utils, TestSuiteSummaryWidget,
  CanvasButtonPopover, EntitySuggestionOption, LineageTabContent:
  core Input, Typography, Badge, Button and Tailwind skeletons
Drop LESS rules that only targeted the removed .ant-* markup.

* chore(playwright): auto-refresh impact-map.generated.json

* chore(ui): restore reactColumnResize mock touched by an accidental lint run

* style(lineage): read theme colours from globals.css instead of --om-* aliases

Lineage LESS, the canvas edge buttons and PortsLineageView now use the
--tw-* colour variables generated from core globals.css, which carry the
light value and flip under .dark-mode. Drop the four --om-color tokens
this branch had added to tokens.css.

* docs(ui): take theme colours from globals.css, not --om-color-*

Point the styling rule and specs README at the --tw-* variables generated
from core globals.css for colours, and note that raw --color-* is invalid
in light mode. Non-colour values keep using tokens.css.

* refactor(lineage): address review feedback on the inline-editing PR

- DotsButton forwards its ref, so LineageNodeMenu drops its wrapper div
  and anchors the add-lineage popover to the button itself; also drop the
  English fallback from its aria-label
- AddLineagePopover: surface search failures with a toast and cancel a
  pending search when the entity type changes
- Move the pipeline-status badge colours, lineage config fields, form
  types and form-value helper to constants / interface / utils
- Use Typography's color prop, Box for the node label row, and Badge's
  default colour
- Drop the column-label block display that stopped Typography's ellipsis

* refactor(lineage): drop the fullscreen z-index and modal z-index overrides

Fullscreen lineage no longer raises itself to the sticky layer, so the
core ModalOverlay stacks above it on its own and the lineage modals no
longer need inline zIndex overrides.

* feat(lineage): keep scene navigation on the main Lineage page

- The lineage map onboarding dialog only opens on the main Lineage page
- Add Lineage to the AI-mode sidebar, pointing at the main Lineage page
- Asset pages stay on their own asset: no semantic zoom band changes,
  band rail, hierarchy breadcrumbs, node drilling, Layer band or lens.
  A Layer-band URL on an asset page falls back to the Asset band.
  Asset and Field (column) levels stay available from the Layers menu
- Move the Layer-band Playwright checks to the main Lineage page and
  assert asset pages no longer drill

* chore(playwright): auto-refresh impact-map.generated.json

* chore(playwright): auto-refresh impact-map.generated.json

* chore(playwright): auto-refresh impact-map.generated.json

* feat(lineage): core-component Layers menu, column toggle on asset tabs, truncation pill

- LineageLayers: rebuild the Layers menu on core Dropdown; the platform page
  picks a lens (Service/Domain/Data Product), asset tabs get a single
  "Column Level" toggle
- LineageMap: replace the "+N more" badge and the untranslated
  knowledge-graph-truncated alert with a "Showing X of Y assets" pill
- PlatformLineage: search via DataAssetAsyncSelectList; drop
  EntitySuggestionOption
- useAsyncDataAssetOptions: escape ES reserved characters, accept multiple
  search indexes

* feat(lineage): keep the scene map on the main Lineage page only

Asset Lineage tabs go back to the classic lineage graph end to end: data
comes from getLineage through the <Lineage /> provider and is drawn by the
new EntityLineageCanvas (the pre-scene ReactFlow renderer with the canvas
edge layer), so asset tabs never call /lineage/scene or carry scene URL
params. The PR's inline editing (node menu, add-lineage popover, column
menu, delete confirmation) is wired to the classic graph.

- Lineage.component: LineageMap for platform lineage, EntityLineageCanvas
  for asset tabs
- Lineage provider: skip the classic fetch on the platform page, restore
  the ReactFlow instance wiring and expose onInitReactFlow, onNodesChange
  and refetchLineage
- LineageLayers: platform lens menu; asset tabs get the classic layers
  (column, observability, service, domain, data product) in the same
  dropdown design
- saveLineageEdge shared by the scene map and the asset canvas

* test(lineage): move asset-tab Playwright coverage back to the classic graph

Asset Lineage tabs load getLineage again, so the asset specs and helpers
wait on /lineage/getLineage instead of /lineage/scene and drive the
classic layer menu instead of scene bands. Platform-page specs keep the
scene.

- helpers: activateColumnLayer uses the Column layer, visitLineageTab,
  column-lineage reloads and connectEdgeBetweenNodes wait on getLineage,
  deleteEdge no longer waits for a scene refetch; config helpers accept
  either endpoint; checkLineageTabActions checks the node menu now that
  edit mode is gone
- restore the pre-scene coverage the scene map had replaced: incident
  counts in the node drawer, the expand/collapse handles and column-layer
  check in LineageSettings, the entity panel's custom-properties tab per
  asset type, node pagination, PNG export size, and pipeline annotations
  read from getLineage
- LineageInteraction: band tests become column-layer tests; drop the
  scene-only hover path highlight, asset-scene persistence and rollup
  edge tests, which have no asset-tab equivalent
- CustomControls: show the lineage time filter on the Lineage tab again,
  since the classic graph honours it

* chore(playwright): auto-refresh impact-map.generated.json

* fix(lineage): node menu in the label, expand handles, edge joins, config modal

- LineageNodeLabelV1: render the node ⋮ menu inside the label header so the
  label keeps the node's full width; long names fill it and truncate with
  an ellipsis
- custom-node.less: only drill-button scene nodes position their content
  box, so the expand/collapse handles are no longer clipped by the node's
  overflow
- CustomNode.utils: the collapse handle is a core Button whose tw:relative
  beat the legacy absolute class; position it with tw:absolute! so it sits
  on the node edge
- CanvasUtils: end node-level edges at the node border now that the
  connection handle that filled the 10px gap is gone
- LineageConfigModal: move the form onto the core HookForm and actually
  invoke the submit handler from the OK button
- CustomControls: core icons instead of the legacy SVG imports
- LineageMap: show asset field counts in scene node footers, and keep the
  status panel on the platform page

* chore(playwright): auto-refresh impact-map.generated.json

* fix(lineage): node card feedback, platform rail and edge anchors

- Base node: light brand card with brand border; no header/footer or
  column dividers; dataflow icon for the column-lineage filter
- Drill button: gray background and icon; breadcrumb pill without shadow
- Platform rail: clickable dots and labels, Layer/Asset/Field stops
- Edge anchors: no target inset for scene nodes; temp node polish and
  breadcrumb-free header
- Core Breadcrumbs: title on truncated items
- Onboarding subtitle copy updated in all locales

* fix(lineage): column search falls back to all columns and survives filter toggle

* fix(lineage): add-lineage panel, hover handles, fullscreen bounds, search loader

- Add upstream / downstream opens a docked side panel with an edge preview,
  a short description and an explicit Add step instead of a node popover
- Asset lineage handles and expand / collapse controls show on hover, focus
  or selection; edges end at the node border
- Rollup edges keep the base stroke; the count badge is a gray pill
- Fullscreen lineage pins to the measured page content column, so it aligns
  in classic and AI mode with the sidebar expanded or collapsed
- Core Autocomplete gains isLoading; the platform search shows a loading
  state instead of "No data found" while results load

* fix(lineage): semantic zoom by screen fill instead of fixed zoom levels

Zooming out goes up a level once the whole graph fills under a third of the
screen, or the zoom reaches its minimum; zooming in goes down a level once a
card fills most of the screen, or the zoom reaches its maximum. Fit to screen
may now zoom out as far as the graph needs.

* fix(lineage): keep the level rail clear of the minimap; edge actions on asset tabs

- The level rail stays centred but never drops into the minimap and zoom
  controls in the bottom-right corner on short viewports
- Edges clicked on an editable asset lineage tab carry isEditable, so the
  edge drawer offers Edit Pipeline and Delete again

* fix(lineage): asset lineage layers read Data Asset Level instead of None

* fix(lineage): add-lineage panel reuses the data asset inputs; fit view on asset tabs

- Type picker is the core FilterSelect with entity icons, asset counts and
  search, as on Explore; the asset search is DataAssetAsyncSelectList
- Fit to screen on asset lineage tabs now reaches the ReactFlow instance,
  and fit padding is a viewport ratio (0.2) instead of 48

* fix(lineage): asset tab parity for node drawer, Delete key and column picks

- Asset lineage node names open the node drawer, as on the platform map
- Delete / Backspace removes the selected node or edge on asset tabs too
- The add-lineage panel loads the picked asset's columns from its search hit
- Playwright add-lineage helper waits for the FQN option, not a search URL

* fix(lineage): inset the layers control from the canvas edge; regular label weight

* fix(lineage): review fixes — view-only platform map, safer delete, temp nodes

- Platform lineage is view and export only: no node/column menus, add panel,
  node/edge delete or Delete key on the map; edge drawer is read-only there
- Delete / Backspace only acts with focus on the page or the canvas, never
  from the drawer, a modal or a menu; a pane click clears the selected edge
- Temp-table nodes are plain: no menu, not deletable, read-only edges
- Deleting a node with several edges no longer keeps removed edges in state
- The add-lineage column picker loads columns by FQN
- Saving lineage settings keeps the rest of the config (pipeline view mode)
- The rail's Data Assets stop works from the Field level
- The platform map no longer recentres on focus coming from overlays

* chore(playwright): auto-refresh impact-map.generated.json

* fix(lineage): CI — core sidebar icon, DotsButton label fallback, keep getColumnHandle

- The AI-mode Lineage module uses the core Dataflow03 icon instead of a new
  raw SVG import (Antd + Less + SVG deprecation guard)
- Dropdown.DotsButton keeps the "Open menu" default label, so screens that
  query the button by name keep working
- Restore the getColumnHandle export openmetadata-collate builds against

* test(playwright): refresh ai-explore baseline for the AI-mode Lineage sidebar entry

* fix(lineage): keep NodeSuggestions and LineageNodeRemoveButton for Collate

openmetadata-collate's ER diagram imports both, so the Collate UI build failed
once this branch removed them. Restored unchanged from main, with the
NodeSuggestions antd suppression (pinned count 496).

* fix(lineage): one toast per failed add; resync after a failed node delete

- saveLineageEdge resolves false when addLineageHandler fails (it already
  toasted), so the asset canvas no longer shows the same error twice
- A failed node delete is caught and the lineage refetched, so a partial
  delete no longer leaves the canvas half-updated or rejects unhandled

* chore(lineage): address SonarCloud findings

- Select / ComboBox: use value + onChange instead of the deprecated
  selectedKey + onSelectionChange
- Drop the unused isConnectable prop from ColumnContent, VirtualColumnList
  and NodeChildren
- LineageColumnMenu: anchor the popover to the DotsButton (it forwards refs)
  and drop the role="presentation" wrapper; react-aria already stops the
  press from reaching the column row
- Narrow LineageNodeLabelV1 sub-component prop types, Set for
  COLUMN_CAPABLE_SEARCH_INDEXES, a ZoomGestureEvent alias, and remove a
  redundant fragment and undefined argument

* fix(lineage): fetch asset lineage once per graph; drop superseded responses

Lineage settings load after mount and re-apply an equal config, which
re-ran the fetch effect while the first request was still in flight: the
dedupe key was only written once a response landed. The asset canvas then
fetched the same graph twice, and when the second response arrived after
the user had switched to the Column layer it redrew with the layers
captured at its call, dropping the column rows and edges.

Claim the key when the request starts, and ignore a response once a newer
request has been issued so the skeleton stays up until the latest graph
lands.

* test(playwright): open lineage menus by hovering the ⋮ button, not the node centre

The middle of a large node can sit under the canvas minimap, which then
intercepts the hover until the test times out.

* test(playwright): align PlatformLineage specs with the classic asset canvas

- Asset-tab Service / Domain views refetch getLineage for the asset's
  service or domain (lineage-layer-service-btn / -domain-btn) instead of
  scene lenses, and nothing is selected by default
- Dismiss the map onboarding on the first /lineage visit: the asset tab no
  longer shows it
- The Lineage Config dialog's confirm button reads "Ok" (label.ok) now that
  it is a core Dialog

* chore(playwright): auto-refresh impact-map.generated.json

* refactor(lineage): core Box, Typography, Card and XClose per review

- Edge drawer close button uses XClose
- Pipeline picker rows, lineage search options and the map rail use Box
  for layout and Typography for text (breadcrumb on the text-xs ramp
  instead of a raw 10px size)
- Add-lineage preview cards use Card (brand for the current asset, dashed
  secondary for the pending one) so both themes come from the component

* style(lineage): node borders fall back to --tw-border-color-primary

Replaces the legacy #b1b1b7 token with the globals.css border token, per
review; dark mode still overrides through --lineage-node-border.

* chore(playwright): auto-refresh impact-map.generated.json

* test(playwright): fix specs broken by shared lineage-branch changes

- ImpactAnalysis: match "Asset Level" exactly; the Layers trigger now reads
  "Data Asset Level"
- ContextCenterMemories: the asset search escapes Elasticsearch-reserved
  characters, so compare the unescaped query
- MetricBulkImportExportEdit: Dropdown.DotsButton keeps the caller's
  aria-label, so the metrics menu is named "Actions", not "Open menu"
- LineageInteraction: fire the column click in place; the minimap can cover
  the column after fit
- PlatformLineage: pick a depth different from the current one; the global
  lineage settings are shared with other specs

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fixes #32000, #33553: Hand confidential OIDC browsers the identity provider's ID token (#34967)

* Mint the configured emailClaim into OpenMetadata-issued tokens

The email-first flow reads a token's address from `emailClaim` alone. Our
own tokens (SAML, LDAP and basic sessions, personal access tokens, and the
confidential OIDC session until it hands out the provider's token) never
carried that claim, so with `emailClaim: upn` JwtFilter fell back to the
legacy claim order and resolved the email's local part (`john.doe` for
user `jdoe`), or failed outright once `jwtPrincipalClaims` was removed as
the deprecation warning suggests. Same class of bug as #32000, through the
email-first flow instead of `jwtPrincipalClaimsMapping`.

Without a mapping, the configured emailClaim now carries the email, the
way the provider's own token does.

* Validate the issuer and audience of identity-provider tokens

JwtFilter checked a provider token's signature and expiry. For tokens not
signed with OpenMetadata's own key it now also requires:
- an audience that includes a configured client: the browser client
  (`clientId`) or the server-side client (`oidcConfiguration.id`);
- for confidential clients, the issuer named in the provider's discovery
  document (a multi-tenant Entra template is filled in from the token's
  `tid`; Google's schemeless issuer is accepted).

The provider's `jwks_uri` from discovery is trusted alongside
`publicKeyUrls`. The SSO settings save writes it there, but a deployment
configured only through the environment never had it. The confidential
sign-in handler registers after JwtFilter at startup and is replaced on a
configuration change, so both values are looked up per use, and only when
the handler serves this filter's client.

Bot tokens and personal access tokens are recognised by OpenMetadata's
signing key id, which key lookup resolves locally first, so tokens minted
under an earlier issuer keep working.

The integration-test harness now presents the provider's ID token as its
API bearer, as the browser does: the mock IdP's access token carries no
audience.

* Hand confidential OIDC browsers the identity provider's ID token

Since 2.0 the confidential OIDC callback minted an OpenMetadata JWT for
the browser. JwtFilter then re-resolved identity from that copy with the
provider-oriented claim configuration, so any claim the copy did not
carry broke login (#32000), and its lifetime came from
`oidcConfiguration.tokenValidity` rather than the provider (#33553).
OpenMetadata now issues tokens only where the protocol gives the browser
none to present: basic, LDAP and SAML.

- Callback: the browser receives the provider's ID token from the code
  exchange. The provider's refresh token is always kept, encrypted, in
  the session.
- Refresh: every refresh redeems that refresh token for the provider's
  next ID token and returns it with its expiry. A rejected grant, a
  session holding no provider refresh token, or a renewal without an ID
  token that outlives the browser's one-minute renewal margin ends the
  session with a 401, which the browser answers by signing in again at
  the provider. A provider that gives no verdict gets a 503; the session
  is kept for a later refresh.
- Login no longer short-cuts an active session with a minted token; the
  provider's own session makes the round trip silent.
- `oidcConfiguration.tokenValidity` and `endSessionWithProvider` are
  deprecated and ignored for OIDC; the SSO form no longer shows them.
  `sessionExpiry` still caps the session, and `maxAge` is still sent to
  the provider. SAML keeps its `tokenValidity`.

The provider's ID token carries no `sessionId`, so logout, revocation and
the per-user session cap take effect at the browser's next refresh, as in
the public OIDC flow. SSOSessionLimit.spec.ts now asserts that.

* Check the refresh-token flow in an SSO Test Login

A confidential OIDC session now renews the browser's ID token by
redeeming the provider's refresh token on every refresh. A provider
that issues no refresh token, refuses its own, or renews without an ID
token would send every user back to sign in each time their ID token
expires, and nothing told the admin beforehand.

Test Login gains a TOKEN_REFRESHED stage, after TOKEN_VALIDATED. It
redeems the test sign-in's refresh token once, with the same grant,
client authentication and token endpoint the live refresh uses, and
fails with the reason and the fix: request offline_access (Okta,
Microsoft Entra ID, Auth0), allow refresh tokens for the client, or for
Google the consent screen. The identity is still resolved when the
stage fails, and a failed Test Login gates Save as before ("Save
anyway" remains the override). The stage is skipped for public OIDC
clients, whose browser renews its own tokens, and for SAML, LDAP and
basic.

The SSO settings label is translated into every locale.

* Cover the unreadable ID token, refused key set and unanswered Test Login refresh

* Revoke the session a new sign-in replaces

A sign-in from a browser that already has a session writes the new
pending session's cookie over the old one. The old session stayed active
until it expired: nothing could reach it any more, but it still counted
toward maxActiveSessionsPerUser, so signing in again on one browser could
evict the user's session on another device.

Login now revokes the active session the browser's cookie names before
creating the pending session, on the MCP flow as well, since that flow
replaces the cookie too. The user's other sessions stay active.

* Chore: Remove Delta Lake connector (#35299)

* chore(ingestion): remove the Delta Lake connector source

Drop the DeltaLake database source package, its example workflow, unit and
integration tests, the deltalake/deltalake-storage/deltalake-spark extras and
the stale lint-baseline entries. Delta Lake as a table format (Trino and
StarRocks detection, _delta_log parsing in Datalake/storage) is untouched.

Amundsen's "delta" databases now map to a CustomDatabase service, matching
how the upgrade migration retypes existing DeltaLake services.

* chore(spec): remove the DeltaLake connection schemas

Delete deltaLakeConnection.json and its deltalake/metastoreConfig.json and
deltalake/storageConfig.json sub-schemas, and drop DeltaLake from the
databaseServiceType enum and the database connection oneOf. TableType.DeltaLake
stays: it describes the table format, not the connector.

UI types and bundled JSON schemas regenerated with json2ts-generate-all.sh and
yarn parse-schema; both produce no diff on the unchanged schemas.

* chore(service): remove the DeltaLake secret converters and test-connection definition

Drop DeltaLakeConnectionClassConverter and StorageConfigClassConverter (the
latter only converted the deltalake StorageConfig), their ClassConverterFactory
entries, the DeltaLake test-connection definition, and DeltaLake from the ODCS
backtick-quoted service types. NestedSecretConverterCoverageTest no longer
allow-lists MetastoreConfig.connection, which is gone with the schema.

* chore(ui): remove the Delta Lake service form, icon and docs

Drop the DeltaLake connection-schema loader, service icon, display labels and
in-app connector doc, and the schema-render test case that rendered the
DeltaLake form. The compact-select behaviour for *Source oneOf fields stays
covered by CoreOneOfField.test.tsx. Remove deltalake from the connector-building
skill's data-lake examples.

* fix(migration): retype DeltaLake services on upgrade

'DeltaLake' is no longer a databaseServiceType, so rows that still carry it
fail to deserialize: listing database services returns an error, and the
2.1.0 data migration itself aborts in DataContractEntityReferenceMigration
when a data contract points at a DeltaLake table.

Before that data migration runs, 2.1.0 schemaChanges.sql (MySQL and
Postgres) now:
- deletes the ingestion pipelines of DeltaLake services together with their
  version snapshots, run statuses and relationships; a pipeline is tied to its
  service only by the service's CONTAINS relationship;
- rewrites serviceType to CustomDatabase on the services, their databases,
  schemas, tables and stored procedures, and on their version snapshots;
- replaces the service connection config with {"type": "CustomDatabase"}, so
  the old metastore password and storage keys are not kept as unmasked
  additional properties;
- deletes the DeltaLake test-connection definition and any DeltaLake
  test-connection workflows.

Every statement is keyed on 'DeltaLake', so a second run changes nothing.
DeltaLakeServiceRetypeSqlMigrationTest runs the shipped statements against
real MySQL and PostgreSQL.

* fix(migration): retire DeltaLake services in a Java step that removes their DAGs

The 2.1.0 SQL deleted DeltaLake ingestion pipeline rows but cannot reach
the pipeline service, so their deployed DAGs stayed scheduled in Airflow.

DeltaLakeRetirementMigration now runs first in the v210 data migration on
MySQL and Postgres, before any step that loads database entities:
- reads the DeltaLake pipeline ids through each service's CONTAINS
  relationship while the services still carry the old type;
- retypes the services, their databases, schemas, tables and stored
  procedures, and their version snapshots, to CustomDatabase in IdBatches,
  replacing a service's connection config with {"type": "CustomDatabase"};
- deletes the DeltaLake test-connection definition and workflows;
- force-deletes each pipeline through IngestionPipelineRepository with the
  migration's pipeline service client, which removes its DAG, statuses,
  relationships and search document; an unreachable runner is logged and
  its DAG left, as a service cascade delete does.

The 2.1.0 schemaChanges.sql files are back to their main content, so ALTER
DATABASE is the last MySQL statement again. The rule is recorded in
ADR:2026-10-13-a-removed-connector-is-retired-in-the-first-data-migration-step.

* chore: drop the DeltaLake retirement decision record

* fix(migration): keep retiring DeltaLake services when a pipeline delete fails

The pipeline ids are read before the retype, the only point a DeltaLake
pipeline can be told apart, and the retype commits first because the
delete loads the pipeline's service, which no longer deserializes while
it is typed DeltaLake. By the time the deletes run their ids are gone
from the database, so a propagated failure stranded every pipeline
behind it with no way for a later run to find them again.

Log the failure with the pipeline id and carry on, and stop the Javadoc
and the decision record claiming the delete resumes like the retypes do.

Restores the decision record the enum removal and the migration ordering
need, which the earlier SQL-to-Java rewrite dropped.

* chore(playwright): auto-refresh impact-map.generated.json

* fix(migration): resume the DeltaLake pipeline delete after a killed run

The pipeline ids cannot be keyed on 'DeltaLake' like every other
statement in this step: they are read before the retype, the only point
a DeltaLake pipeline can be told apart, and the handle autocommits, so
the retype has landed by the time the deletes run. A run killed inside
the delete loop therefore left its remaining pipelines and their
deployed DAGs behind for good, because the next run's query finds no
DeltaLake service to name them.

Write each id to entity_extension as deltaLakeRetirement.pendingPipeline
before the retype and clear it once its delete has been attempted, so a
killed run finds its leftovers on the next start. The marker is keyed on
the pipeline's own id, so a hard-deleted pipeline takes it along.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fixes #34689: record every Assets tab edit on the asset as its own PATCH (#35026)

* feat(entities): apply a bulk edit as one authorized PATCH per entity

* fix(entities): index a patched entity with the fields it inherits

* fix(assets): save every Assets tab edit as the asset's own PATCH

* fix(search): keep a table's tags and data products off its column entries

* docs(decisions): record how bulk edits write, authorize and index

* fix(entities): fail only the group that cannot be loaded or recorded

* fix(assets): refuse an asset whose labels clash with its glossary's tags on the glossary term tab

* refactor(assets): derive the Assets tab status with the shared bulk status rule

* test(assets): drop the fail-before-pass gate test that per-asset saves replace

* test(assets): compare tab and PATCH labels without their appliedAt

* test(assets): keep the search entry checks clear of the response cache and of unordered updates

* test(assets): check the column entries of all tables in one search per poll

* test(assets): drop the domain move check that waits on main's lossy column propagation

* fix(assets): authorize the tag tab job with the request's impersonation

* fix(ui): report assets the server refuses to remove from an Assets tab

* chore(playwright): auto-refresh impact-map.generated.json

* fix(ui): ask to refresh when a tag tab removal's outcome does not arrive

* test(glossary): expect a refused bulk asset row instead of a 403

* fix(search): send column entries the glossary terms they inherit from their table

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fixes #34249: Add Neo4j database connector (#34250)

* feat(ingestion): Add Neo4j database connector

Ingest a Neo4j graph schema as a database service: node labels become tables
in a `nodes` schema and relationship types become tables in a `relationships`
schema, with their properties as columns typed from
db.schema.nodeTypeProperties() / db.schema.relTypeProperties().

- Connection schema (scheme, hostPort, username, password, databaseName,
  includeRelationships, propertyFilterPattern and the standard filters) and
  a test connection definition (CheckAccess, GetDatabases, GetNodeLabels,
  GetRelationshipTypes)
- Source built on CommonNoSQLSource with declared property types, the
  multi-label NOT NULL rule, a property filter, and a failed schema read
  skipping only that schema
- BaseConnection with test-connection checks and an error pack for the
  neo4j driver's status codes
- `neo4j` pip extra, CLI workflow example, unit tests
- UI: connection form schema, service icon, side-panel docs, Beta flag and
  regenerated TypeScript types

* fix(ingestion): read the Neo4j graph schema once and keep failed schemas out of deletion

The post-process deletion hooks list schemas again through
get_schema_name_list(), which re-queried Neo4j. A schema whose read failed
during the walk could then reappear and have all its tables soft-deleted, a
persistently failing schema was soft-deleted when markDeletedSchemas is on,
and each database ran the schema procedures up to four times.

Cache the graph schema per database, record which schemas failed, and add
failed schemas to the schema source state before the schema deletion sweep.

* Address Neo4j connector review feedback

- Silence driver notifications so schema reads don't log a deprecation warning each run
- Reuse the shared neo4j pin (now ~=5.7, needed for notifications_min_severity)
- Fix the GetNodeLabels / GetRelationshipTypes test-connection descriptions
- Drop the unused databaseFilterPattern from the connection schema
- Note the upcoming propertyTypes format change next to the schema queries

* Fix py-checkstyle and regenerate UI types for the Neo4j connector

- Use lazy %-style logging for the filtered-property debug message (G004)
- Regenerate TS types and dereferenced schemas with json2ts-generate-all.sh and parse-schema

---------

Co-authored-by: michael gong <209566851+michaelgong428@users.noreply.github.com>
Co-authored-by: Khairajani <himanshukhairajani8@gmail.com>

* fix(lineage): strip FQN quotes from schema/db/table names before applying view filters (#34365) (#34568)

* fix(lineage): strip FQN quotes from schema/db/table names before applying view filters

Fixes #34365

`yield_es_view_def` splits each view's `fullyQualifiedName` with `fqn.split()`,
which preserves double-quoted identifiers verbatim.  A schema named `folder.sub`
(containing a dot) becomes `'"folder.sub"'` in the split result.
`view_lineage_producer` passed that quoted string directly to `filter_by_schema`,
so a pattern like `^folder\.sub$` never matched and every view in the schema was
reported as *View Filtered Out* (includes filter) or passed through unchecked
(excludes filter).

The fix calls `fqn.unquote_name()` on all three name fields before passing them
to the filter helpers.  `unquote_name` strips outer double-quotes only when they
are present, so plain names are unaffected.

Two unit tests added:
- Verifies that an includes-pattern on the unquoted schema name keeps the view.
- Verifies that an excludes-pattern on the unquoted schema name filters it out.
- Existing view-lineage-producer tests continue to pass.

Prepared with AI assistance (Claude Code, Anthropic), reviewed for correctness before submission.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ingestion): pass Python checkstyle for view lineage PR

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Harsh Soni <64592571+harshsoni2024@users.noreply.github.com>
Co-authored-by: harshsoni2024 <harshsoni2024@gmail.com>

* Fix TagPage test mock, refresh ai-explore baseline, record Classic deprecation

- TagPage.test mocks @openmetadata/ui-core-components without Badge, which
  TabsLabel's count now renders; add it from the real module.
- ai-explore visual baseline predates the Lineage nav item #34551 added on
  main (its own merge-queue run failed the same snapshot). The new baseline is
  CI's actual render from the pinned Playwright container, pixel-identical with
  and without this PR.
- Add the decision record for the Classic mode deprecation notice.

* chore(playwright): auto-refresh impact-map.generated.json

* Update generated TypeScript types and dereferenced schemas

* Reset bot-generated schema JSON and impact map to main

They were generated from the branch before the replayed main commits were
dropped; this PR changes no schemas, and CI regenerates the impact map.

* chore(playwright): auto-refresh impact-map.generated.json

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rohit Jain <60229265+Rohit0301@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Teddy <teddy.crepineau@gmail.com>
Co-authored-by: Anujkumar Yadav <anujf0510@gmail.com>
Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com>
Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
Co-authored-by: Akash Verma <138790903+akashverma0786@users.noreply.github.com>
Co-authored-by: Adrià Manero <adria.estivill@getcollate.io>
Co-authored-by: michaelgong428 <michael.gong@finsure.com.au>
Co-authored-by: michael gong <209566851+michaelgong428@users.noreply.github.com>
Co-authored-by: Khairajani <himanshukhairajani8@gmail.com>
Co-authored-by: zerafachris <christopher.zerafa@blocklabs.io>
Co-authored-by: Harsh Soni <64592571+harshsoni2024@users.noreply.github.com>
Co-authored-by: harshsoni2024 <harshsoni2024@gmail.com>

This branch had an error being deployed

1 failed deployment
test — ca43a43d Deployed Oct 16, 2026 by manerow via RDF Playwright execution #33682
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backward-Incompatible-Change 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.

Changes made from an Assets tab are not recorded on the asset

4 participants