Skip to content

fix(security): third batch of backend security-hardening fixes - #36952

Merged
mohityadav766 merged 49 commits into
mainfrom
fix/security-hardening-3
Nov 4, 2026
Merged

mohityadav766 merged 49 commits into
mainfrom
fix/security-hardening-3

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

A third batch of backend security-hardening fixes from an internal review, one commit per area:

  • Data access: prefix-rename statements.
  • Sign-in and sessions: token identity, logout, password changes, LDAP role sync, SAML login.
  • Security configuration writes.
  • Input handling: validation patterns, JSON Patch limits, description sanitization.
  • User accounts: restore, self sign-up, deletion, role and bot flags.
  • Policies: disabled and team policies, reviewers, policy and role writes.
  • Write authorization: PATCH, create and PUT, system tags.
  • Hard-delete cleanup.
  • Reads: lineage, listings, version history, reference lists, standalone read endpoints.
  • Search: pipeline runs, column entries, Data Insights charts, vector search, user emails.

Each commit carries its own regression tests. The rules they set are recorded under docs/decisions/; details are tracked internally.

Upgrade notes:

  • Disabled policies no longer apply, and a team's policies grant their allow rules only to the team's members.
  • Changing a password ends the user's other sign-ins; personal access tokens stay valid.
  • SAML sign-in completes through one extra redirect.
  • PATCH requests that replace a whole entity, or exceed the size limits, are refused.
  • Users cannot delete their own account.
  • Search no longer returns or matches user emails for callers who are not admins.
  • Run a search reindex after upgrading.

Tracking: open-metadata/openmetadata-collate#8018

🤖 Generated with Claude Code

- The FQN prefix renames (glossary, classification, tag, domain and container
  descendants), the tag-usage prefix rewrites and the thread-link rename bind
  every value; only the table name stays a template attribute. A guard test
  fails on any DAO statement that is a bare template attribute.
- MySQL swaps an FQN prefix by length instead of by regular expression, and
  the JSON-text rewrites match names containing quotes or backslashes, so a
  container move below a quoted name renames its descendants.
- Tag lists count usage with the bound per-tag query, so usageCount matches
  what a GET returns.
- A token verified against OpenMetadata's own key resolves to the account
  named in its sub claim, or in username when the configured claims put the
  email into sub, instead of through the principal-claim order configured
  for the identity provider's tokens.
- The authorizer uses the authenticated principal name whole, not the part
  before the first '/' or '@'.
…nd sign-ins

- Logout retires the bearer token the filter verified for the request and
  ignores the token in the body. The logout cache keys entries by digest,
  keeps only the user and logout time, skips tokens that never expire, and
  holds at most 100 000 entries, dropping the one that expires first.
- A password reset, a user's own change, an admin's change and the
  reset-password ops command end the user's refresh tokens, live sessions
  and MCP grants. Personal access tokens and bot tokens stay.
…es in its own browser

- An LDAP sign-in applies the directory groups the user is in now, a user in
  no group included, and reports a change of the admin flag as well as of the
  roles so the account is saved. A configured admin principal stays admin.
- A SAML login records the id of the request it sends. The ACS accepts only
  the answer to that request, attaches the account to the pending login and
  redirects to /api/v1/saml/complete, which activates the session for the
  browser holding that login's session cookie.
- The security configuration GET masks secrets for every caller.
- A PATCH applies its operations to the configuration as an admin reads
  it, with the secrets masked.
- PATCH and PUT restore a masked secret only while the setting it is used
  with is unchanged, and otherwise ask for it to be entered again.
- The email and entity-link patterns match in linear time, accepting the
  same inputs as before.
- A JSON Patch body is read within fixed size, nesting and operation
  bounds, for the REST reader and the MCP tool alike, and applying a patch
  refuses one that grows the document beyond a fixed budget.
- A JSON Patch on the document root is refused.
- The description sanitizer restores entity-link tokens in a single pass,
  only where the token is element text.
- A PUT naming the caller's own account is authorized explicitly: a deleted
  self gets 404, an unknown self is a sign-up on the same terms as POST.
- A role change on a user write needs an admin in either direction; a
  non-admin PUT keeps the stored roles, admin flag and bot flag.
- Deleting your own account needs an admin, on all three delete endpoints.
- A user CSV row from a non-admin is refused for a stored admin or bot.
…BAC writes

- Setting owners on configuration entities (policies, teams, apps, workflows, alerts, ingestion
  pipelines, classifications, services) needs EditAll; ownership tasks use the same mapping.
- A reviewer is matched by user id.
- A disabled policy contributes no rules.
- An owning team's policies contribute only their deny rules to callers outside the team.
- PolicyResource no longer rewrites OrganizationPolicy on startup.
- Policy and role writes are identity endpoints; BotNonAdminImpersonationPolicy also denies bot
  targets, with a 2.1.0 data migration for stored policies.
- Team and role policy conditions are rewritten on hard delete only; soft delete and restore
  drop the cached policies.

ADR:2026-11-03-owners-of-configuration-entities-need-edit-all
ADR:2026-11-03-a-reviewer-is-matched-by-user-id
ADR:2026-11-03-a-disabled-policy-has-no-rules
ADR:2026-11-03-an-owning-teams-allow-rules-stay-with-its-members
ADR:2026-11-03-seeded-policies-are-not-reset-on-startup
ADR:2026-10-26-impersonated-requests-never-reach-credentials-or-grant-privileges
ADR:2026-10-30-deleted-policy-condition-references-become-false
- A move or copy is authorized at its from location as well as its path.
- A field name selects a narrower operation only where the entity's type declares that field;
  below a map or an untyped object a write needs EditAll.
- Tag labels are compared together with the object that carries them, on the entity the
  repository would store; touching tags without a net change needs EditTags, an empty patch
  needs ViewBasic, and a glossary term is always EditGlossaryTerms.
- The user, notification-template, context-file and conversation patch guards check every
  touched location.
- Non-admin callers cannot copy or move a value out of a service connection or a pipeline's
  source config.
- Date fields refuse object and array values.

ADR:2026-11-03-a-patch-is-authorized-at-every-location-it-touches
- On create, hasDomain() needs a domain of the stored container and every domain the request
  names.
- A root parent reference without an id is loaded by name and a missing one fails the request;
  TagMapper resolves the classification by name.
- A tag's parent must belong to the tag's classification.
- PUT contexts see soft-deleted entities, as the write does.
- A system tag's fully qualified name cannot change through its parent or classification either.

ADR:2026-11-03-a-create-is-authorized-against-its-stored-container
- The FQN-prefix cleanup of tag labels and field relationships runs only for taggable types and
  for types, whose custom property definitions live under their name.
- Custom property definitions are removed only when their type is deleted.
- The tag-FQN label delete without a source is removed; tags and glossary terms keep removing
  their own source-bound labels.

ADR:2026-11-03-a-hard-delete-removes-only-rows-of-the-deleted-entity
…check what they return

- The lineage graph cache stores graphs serialized and hands each reader its own copy, so a
  caller's filtering never changes the graph the next caller gets.
- Lineage hydrate loads entities with every attribute a policy condition reads, requires the view
  operations of the requested fields (shared with GET through ViewOperations), and trims the
  entities back to the requested fields.
- The column read and the ontology endpoints authorize copies loaded with the authorization
  fields; ontology pack install authorizes its target glossary by id.

ADR:2026-11-03-every-read-path-checks-the-fields-it-returns
ADR:2026-10-30-search-lineage-returns-only-assets-the-caller-can-view
…w row-level rules

- Tag conditions read the tags of inline children (columns, tasks, schema fields, features) on
  every read path, from a copy loaded with each repository's policy tag projection; nested struct
  columns and fields are merged at every depth.
- Listings check each row when a row-dependent rule could refuse it, refill short cursor pages
  from the following pages (up to 5 reads or 1000 rows) and seal their cursors; offset and search
  listings, glossary-term lists and test-suite lists drop such rows.
- History keeps versions of entities the caller may view; memories and files also apply their
  own visibility.
- Reference lists (user and team owns, follows, children, descendants, parents; schema tables;
  database schemas) keep only entities the caller may view.

ADR:2026-11-03-tag-conditions-see-the-tags-of-inline-children
ADR:2026-11-03-a-listing-returns-only-rows-the-caller-may-read
ADR:2026-11-03-a-reference-list-names-only-entities-the-caller-may-view
- Conversations and tasks read by id, with their replies and versions, also need ViewBasic on the
  entity they are about; a refused conversation answers 404.
- Activity by entity link matches the link's entity type and keeps only viewable events.
- Team hierarchy, task counts, domain hierarchy and the type custom-property reads need ViewBasic
  on their type; the team hierarchy drops teams the caller may not view and still serves joinable
  teams to someone signing up. The role list uses the standard listing.
- The global profiler setting is always authorized on tables.

ADR:2026-11-03-read-handlers-authorize-like-the-entity-reads-they-mirror
- Pipeline run documents carry the pipeline's owners, domains, tags (tasks' included), tier,
  certification and service; the run index is covered by rules on pipelines. A pipeline's access
  field change re-stamps its runs, and reindexing pipelines re-stamps the runs of each batch.
  Table-observability documents in the same index carry the pipeline's fields.
- Table and column documents carry policyTags, every tag the table's endpoints decide on;
  matchAnyTag and matchAllTags match a document's own labels or its policyTags. Under access
  control a table's tags-only change rebuilds its column documents.
- Live Data Insights charts aggregate under the caller's search access policies; snapshot charts
  and previews drop a restricted caller's own filter; a streaming session is joined only by its
  user with the same filter.
- Vector search keeps only hits the caller may view.
- For callers other than admins and bots, search leaves out user emails and refuses email
  lookups; audit-log and alert event listings mask user emails.
- Run-index mappings gain the copied fields; table and column mappings gain policyTags (all
  languages).

ADR:2026-11-03-search-applies-the-same-access-rules-as-entity-reads
ADR:2026-10-08-column-entries-carry-only-their-own-tags
@mohityadav766
mohityadav766 requested a review from a team as a code owner November 3, 2026 13:02
@mohityadav766
mohityadav766 requested a review from IceS2 November 3, 2026 13:02
@github-actions

github-actions Bot commented Nov 3, 2026

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.

The ACS stores the hash of a one-time code on the pending login and redirects to /api/v1/saml/complete with that code. Completing the login needs both the session cookie and the code.
For callers who do not read user emails, a query or filter that runs a script is refused like one that names an email field, since a script can build a field name at run time.
A refresh replaces its refresh token with the new one in a single UPDATE keyed by the old token, for basic and LDAP sign-ins. Only the request whose update changed the row gets new tokens; a token deleted while the request ran is not renewed.
Deleting a type still removes its custom property definitions, and no longer touches tag_usage, whose rows record no entity type.
# Conflicts:
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java
# Conflicts:
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java
#	openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java
#	openmetadata-service/src/test/java/org/openmetadata/service/search/indexes/BuildSearchIndexDocTest.java
Comment on lines +278 to +283
// Owners of a configuration entity, such as a service, need EditAll as well as EditOwners
const configEffect =
config.testId === 'edit-owner' && options.ownerEditNeedsEditAll
? 'deny'
: effect;
await checkElementVisibility(testUserPage, config, configEffect);

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.

Why one needs explicite EditAll despite EditOwner?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Setting owners is EditOwners, but OrganizationPolicy also gives an entity's owner All on it. On configuration types (services, policies, teams, apps, workflows, ingestion pipelines, classifications, test definitions, ...) owning the entity means controlling its configuration (a service's connection, a policy's rules, a team's default roles), so this PR treats setting their owners as a configuration change: it needs EditAll as well as EditOwners, and a rule on EditOwners still applies. Data assets are unchanged; EditOwners alone is enough there.

The permissions response follows the same rule, so for a user with EditOwners but not EditAll the service page no longer offers the owner edit, which is what this spec now expects. Details are in docs/decisions/2026-11-03-owners-of-configuration-entities-need-edit-all.md. I reworded the comment in the test to say this (2133ba8).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Update: dropped this from the PR (edc97c0). Setting owners asks for EditOwners on every entity type again, as on main; who holds it is up to the configured policies. The spec and helper are back to their main versions.

…onfigured policies

Setting owners asks for EditOwners on every entity type, as before this branch. Who holds EditOwners is decided by the policies, which admins configure.
# Conflicts:
#	openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TeamResourceIT.java
#	openmetadata-service/src/main/java/org/openmetadata/service/security/ViewPermissionFilter.java
@gitar-bot

gitar-bot Bot commented Nov 4, 2026

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

🔴 High risk · Hardening authorization across reads, writes, search, account changes, and SAML/session flows could expose data or permit unintended access if checks regress.

Third batch of backend security-hardening fixes covering data access, sessions, policies, write authorization, search, and reads, with each area carrying its own regression tests. Resolved this session: filtered listing first page now returns a non-null before cursor, BoundedJsonPatch no longer loses decimal precision via double, and PUT requests that omit teams no longer count as leaving closed teams. No open issues found.

✅ 3 closed
✅ Bug: Filtered listing's first page gets a non-null before cursor

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java:440-448
When the listing filters rows per caller, readableRowsPage sets the reverse cursor (backAgain) to cursorOf(kept.getFirst()) whenever any row is kept. On a forward first page (page.after() == null) that produces a non-null paging.before. EntityRepository.listAfter deliberately returns before = null there ("genuine first page — no page before it"). Clients treat a non-null before as meaning a previous page exists, so restricted users see an enabled "Previous" control on page one, and following it returns an empty page. The fix is to carry first.getBefore() through when the request had no after cursor, so a null stays null.

✅ Edge Case: BoundedJsonPatch round-trips decimals through double, losing precision

📄 openmetadata-service/src/main/java/org/openmetadata/service/util/BoundedJsonPatch.java:40-49 📄 openmetadata-service/src/main/java/org/openmetadata/service/util/BoundedJsonPatch.java:99-110
BoundedJsonPatch parses with a default Jackson ObjectMapper, which reads floating-point numbers as DoubleNode. It then re-serializes the tree with node.toString() for the jakarta reader. The jakarta reader used before kept BigDecimal precision, so this round trip changes values:

  • a patch value such as 12345678901234567.891 (for example a decimal custom property) is silently rounded before it is stored;
  • a value such as 1e999 becomes Infinity, which is not valid JSON. The re-parse then throws a jakarta JsonParsingException. toArray does not catch it, so the REST reader returns 500 instead of 400, and the MCP tool reports a server fault.

Fix: enable USE_BIG_DECIMAL_FOR_FLOATS on MAPPER, and set a JsonNodeFactory with exact decimals so the bounded parse leaves values unchanged.

✅ Bug: PUT that omits teams counts as leaving every closed team

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java:1383-1391 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java:1437-1445 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java:973-986
grantsPrivileges keeps going whenever the request names a role or a domain, and then calls changesClosedTeamMembership(currentUser, create.getTeams()). With the old joinsClosedTeam, a null or empty team list joined nothing. Under the new logic, an empty requested set puts every stored non-organization team in left. So a non-admin who is in any non-joinable team and sends PUT /v1/users with their roles or domains but no teams is treated as leaving those teams. authorizeAdminForPrivilegedFields then refuses the request.

That result contradicts the code a few lines later (lines 983-985) and its comment, which say an omitted team list keeps the stored teams. The membership never actually changes, yet the update is rejected. The fix is to treat an omitted list as "no change" for existing users, the same way changesRoles handles omitted roles.

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

@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

This branch was previously deployed

1 inactive deployment
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.

4 participants