Skip to content

fix(domain): scope REST lists by a global X-OpenMetadata-Domain header (server-side, central) - #31538

Draft
sonika-shah wants to merge 1 commit into
open-metadata:mainfrom
sonika-shah:fix/global-domain-header
Draft

fix(domain): scope REST lists by a global X-OpenMetadata-Domain header (server-side, central)#31538
sonika-shah wants to merge 1 commit into
open-metadata:mainfrom
sonika-shah:fix/global-domain-header

Conversation

@sonika-shah

Copy link
Copy Markdown
Collaborator

Summary

Replaces the per-API ?domain= approach to the global (navbar) domain filter with a single
X-OpenMetadata-Domain request header applied server-side and centrally, mirroring how the
active-persona header works. This is the scalable "right solution" to the gap that #31180 patched
per-endpoint (glossary/metric/tag/classification/service/dataProduct): every REST list view is
covered in one place, with no per-resource ?domain handling.

Phase 1 (REST list path). Opening as draft for approach review — see Scope below.

Problem

The navbar domain filter didn't scope REST list endpoints: the UI stamped ?domain=<fqn> on every
list request, but each resource had to declare @QueryParam("domain") and apply it — only a handful
did, so most lists ignored it. Adding the param to every API (and every future entity) doesn't scale,
and the same gap shows up on entity-page child lists.

Approach — mirror the active-persona header

  • Transport: the UI (withDomainFilter) sends X-OpenMetadata-Domain: <domainId> on REST GETs
    instead of ?domain=.
  • Read once: the three auth filters (JwtFilter, NoopFilter,
    CatalogOpenIdAuthorizationRequestFilter) read the header into CatalogSecurityContext.activeDomain
    • an ActiveDomainContext ThreadLocal (cleared in PerRequestContextCleaner) — same shape as
      X-OpenMetadata-Persona / ActivePersonaContext.
  • Apply centrally: EntityUtil.addDomainQueryParam (already the domain chokepoint for every
    EntityResource.listInternal list) reads the active domain and scopes the list.

Semantics

  • Strict narrowing: when a domain is selected, only that domain's assets are listed (un-domained
    hidden), for admins and DomainOnlyAccessRole users. When "All Domains" is selected (no header),
    behavior is unchanged (admins unfiltered; restricted users keep their RBAC scope incl. un-domained).
  • RBAC-safe (cannot bypass): the accessible set always comes from subjectContext.getUserDomains()
    server-side; the header only narrows within it (intersection). A domain the user can't access →
    no-match. The header can only shrink the result set, never widen it.
  • Guarded: applied only to entity types that carry a domain (supportsDomains), and a governance
    denylist (user, team, role, policy, persona, bot) is excluded even though those carry a
    domains field — otherwise selecting a domain would empty the Users/Teams/Policies lists.

Scope

  • This PR — REST list path. Search is intentionally unchanged: it already scopes domains
    securely server-side (RBACConditionEvaluator.hasDomain from user.getDomains(), non-bypassable),
    and the nav-domain narrowing stays client-side in query_filter (safe because RBAC bounds it). No
    server-side search change is needed.
  • Follow-up (Phase 2): the few DB-list endpoints that bypass listInternal and would need the
    same central apply (GlossaryTermResource, custom-paging resources).

Verification

Built into a stack and ran an API matrix with the header, for admin and a DomainOnlyAccessRole
user with domains [A,B]
; glossaries in A, in B, and un-domained:

Context Header Result
admin none A + B + un-domained
admin B only B (A + un-domained hidden — strict)
restricted [A,B] none A + B + un-domained (RBAC lenient)
restricted [A,B] B only B
restricted [A,B] A only A
restricted [A,B] C (inaccessible) nothing (RBAC not bypassed)
any B, on users/teams/policies/roles unaffected (guard)

All pass. The customer's original case (a Domain-A glossary hidden when Domain B is selected) works
for both admin and the restricted user.

Notes

Replaces the per-API ?domain= approach with a persona-style header applied
server-side and centrally. Phase 1 (REST list path):

- ActiveDomainContext ThreadLocal + X-OpenMetadata-Domain header read in the
  three auth filters into CatalogSecurityContext.activeDomain, cleared in
  PerRequestContextCleaner.
- EntityUtil.addDomainQueryParam applies the nav domain STRICT (un-domained
  hidden) for admins and DomainOnlyAccess users; intersects with getUserDomains()
  for restricted users so it can only narrow, never bypass RBAC; guards on
  supportsDomains + a governance denylist (user/team/role/policy/persona/bot).
- UI withDomainFilter sends the header (domain id) on REST GETs instead of
  ?domain=; search keeps its client-side query_filter for now (Phase 1.5).

Verified on a stack: admin/restricted x {B,A,inaccessible,none}, strict semantics,
RBAC no-bypass, governance lists unaffected.
@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Aug 14, 2026
@sonika-shah
sonika-shah marked this pull request as ready for review August 14, 2026 12:16
Copilot AI lite review requested due to automatic review settings August 14, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@github-actions

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.

Comment on lines +1106 to +1119
String navDomainId = ActiveDomainContext.getActiveDomain();
if (!nullOrEmpty(navDomainId) && entitySupportsDomains(entityType)) {
String requested = "'" + navDomainId.replace("'", "") + "'";
// STRICT narrowing (no domainAccessControl -> un-domained assets are hidden), for admins and
// restricted users alike. For restricted users the selection is intersected with their
// accessible domains so it can only narrow, never widen (a domain they cannot access -> no
// match).
String domainId =
restricted
? intersectWithAllowedDomains(
requested, getCommaSeparatedIdsFromRefs(subjectContext.getUserDomains()))
: requested;
filter.addQueryParam("domainId", domainId);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Edge Case: Sub-domain assets hidden on REST lists when parent domain selected

The central REST path resolves the header to an exact domainId IN (...) match (ListFilter.getDomainCondition builds entity_relationship.fromId IN (...) with no descendant expansion), so selecting a parent domain in the navbar hides assets that belong to its sub-domains. The search path in the same interceptor (withDomainFilter.tsx:94-98) instead matches with a prefix: '<fqn>.' clause, so Explore includes sub-domain assets. This produces inconsistent results between list views and search for the same navbar selection. Consider expanding the requested domain to include its sub-domain ids server-side before building the filter, mirroring the search prefix semantics.

Was this helpful? React with 👍 / 👎

Comment on lines +18 to +24
/**
* Holds the global (navbar-selected) domain sent on the request via the {@code X-OpenMetadata-Domain}
* header, so code paths that don't have the {@link jakarta.ws.rs.core.SecurityContext} at hand can
* still read it. Mirrors {@link ActivePersonaContext}. The value is a single domain fully-qualified
* name; it is only ever used to NARROW a listing within the user's accessible domains, never for
* authorization.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: ActiveDomainContext javadoc says FQN but value is a domain id

The class comment states "The value is a single domain fully-qualified name", but the UI sends activeDomainEntityRef.id (a UUID) and EntityUtil treats the value as a domainId for entity_relationship.fromId matching. The misleading doc will confuse future maintainers into passing/comparing FQNs. Update the javadoc to say the value is the domain id (UUID).

Was this helpful? React with 👍 / 👎

Comment on lines +1142 to +1151
private static boolean entitySupportsDomains(String entityType) {
if (NAV_DOMAIN_FILTER_EXCLUDED_TYPES.contains(entityType)) {
return false;
}
try {
return Entity.getEntityRepository(entityType).isSupportsDomains();
} catch (Exception e) {
return false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: Broad catch(Exception) in entitySupportsDomains swallows errors silently

entitySupportsDomains catches the broad Exception and returns false with no logging, which both violates the project rule against broad catches and silently disables domain scoping for any entity type whose repository lookup throws for an unexpected reason. Narrow the catch to EntityNotFoundException (the expected case for unknown types) and at least log unexpected failures with context.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 3 findings

Replaces the per-API domain parameter with a global X-OpenMetadata-Domain header applied centrally on REST lists, but sub-domain assets are incorrectly hidden when a parent domain is selected, ActiveDomainContext javadoc is inaccurate, and entitySupportsDomains uses a broad catch block.

⚠️ Edge Case: Sub-domain assets hidden on REST lists when parent domain selected

📄 openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java:1106-1119 📄 openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx:94-98

The central REST path resolves the header to an exact domainId IN (...) match (ListFilter.getDomainCondition builds entity_relationship.fromId IN (...) with no descendant expansion), so selecting a parent domain in the navbar hides assets that belong to its sub-domains. The search path in the same interceptor (withDomainFilter.tsx:94-98) instead matches with a prefix: '<fqn>.' clause, so Explore includes sub-domain assets. This produces inconsistent results between list views and search for the same navbar selection. Consider expanding the requested domain to include its sub-domain ids server-side before building the filter, mirroring the search prefix semantics.

💡 Quality: ActiveDomainContext javadoc says FQN but value is a domain id

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/ActiveDomainContext.java:18-24 📄 openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx:110-112

The class comment states "The value is a single domain fully-qualified name", but the UI sends activeDomainEntityRef.id (a UUID) and EntityUtil treats the value as a domainId for entity_relationship.fromId matching. The misleading doc will confuse future maintainers into passing/comparing FQNs. Update the javadoc to say the value is the domain id (UUID).

💡 Quality: Broad catch(Exception) in entitySupportsDomains swallows errors silently

📄 openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java:1142-1151

entitySupportsDomains catches the broad Exception and returns false with no logging, which both violates the project rule against broad catches and silently disables domain scoping for any entity type whose repository lookup throws for an unexpected reason. Narrow the catch to EntityNotFoundException (the expected case for unknown types) and at least log unexpected failures with context.

🤖 Prompt for agents
Code Review: Replaces the per-API domain parameter with a global X-OpenMetadata-Domain header applied centrally on REST lists, but sub-domain assets are incorrectly hidden when a parent domain is selected, ActiveDomainContext javadoc is inaccurate, and entitySupportsDomains uses a broad catch block.

1. ⚠️ Edge Case: Sub-domain assets hidden on REST lists when parent domain selected
   Files: openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java:1106-1119, openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx:94-98

   The central REST path resolves the header to an exact `domainId IN (...)` match (ListFilter.getDomainCondition builds `entity_relationship.fromId IN (...)` with no descendant expansion), so selecting a parent domain in the navbar hides assets that belong to its sub-domains. The search path in the same interceptor (withDomainFilter.tsx:94-98) instead matches with a `prefix: '<fqn>.'` clause, so Explore includes sub-domain assets. This produces inconsistent results between list views and search for the same navbar selection. Consider expanding the requested domain to include its sub-domain ids server-side before building the filter, mirroring the search prefix semantics.

2. 💡 Quality: ActiveDomainContext javadoc says FQN but value is a domain id
   Files: openmetadata-service/src/main/java/org/openmetadata/service/security/ActiveDomainContext.java:18-24, openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx:110-112

   The class comment states "The value is a single domain fully-qualified name", but the UI sends `activeDomainEntityRef.id` (a UUID) and EntityUtil treats the value as a domainId for `entity_relationship.fromId` matching. The misleading doc will confuse future maintainers into passing/comparing FQNs. Update the javadoc to say the value is the domain id (UUID).

3. 💡 Quality: Broad catch(Exception) in entitySupportsDomains swallows errors silently
   Files: openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java:1142-1151

   `entitySupportsDomains` catches the broad `Exception` and returns false with no logging, which both violates the project rule against broad catches and silently disables domain scoping for any entity type whose repository lookup throws for an unexpected reason. Narrow the catch to `EntityNotFoundException` (the expected case for unknown types) and at least log unexpected failures with context.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ UI Checkstyle passed — lint findings in changed files

🔍 ESLint findings in this PR's files — 0 error(s), 2 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 2 warning(s) across 1 changed file(s).

Count Rule
1 sonarjs/cognitive-complexity
1 sonarjs/cyclomatic-complexity
All findings
Location Rule Message
🟡 src/hoc/withDomainFilter.tsx:30:31 sonarjs/cognitive-complexity Refactor this function to reduce its Cognitive Complexity from 30 to the 15 allowed.
🟡 src/hoc/withDomainFilter.tsx:30:31 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 14 which is greater than 10 authorized.","cost":4,"secondaryLocations":[{"line":30,"column":30,"endLine":30,"endColumn"

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

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

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants