Skip to content

fix: harden entity pagination, multi-owner authz, and cursor charset#29750

Open
mohityadav766 wants to merge 6 commits into
mainfrom
mohit/backend-hardening-review
Open

fix: harden entity pagination, multi-owner authz, and cursor charset#29750
mohityadav766 wants to merge 6 commits into
mainfrom
mohit/backend-hardening-review

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Jul 5, 2026

Copy link
Copy Markdown
Member

Three targeted correctness fixes surfaced during a downstream backend review, each traced end-to-end.

  • EntityRepository.listAfter/listBefore — guard empty result pages. A client holding a valid cursor whose remaining rows were concurrently deleted hit entities.get(0) / get(size-1) on an empty list → IndexOutOfBoundsException → HTTP 500. Now returns an empty page with null cursors.
  • SubjectContext.isTeamAsset — evaluate all owners instead of returning inside the first loop iteration. A multi-owner asset [user, teamB] (or [teamA, teamB]) with a matchTeam() policy on teamB previously checked only the first owner and wrongly denied teamB members access to an asset their team owns. Fail-closed regression, now fixed (OR across owners).
  • RestUtil.decodeCursor — decode Base64 cursor bytes as UTF-8 to match encodeCursor. On JVMs whose default charset is not UTF-8, pagination cursors for non-ASCII entity names round-tripped incorrectly.

Compiles clean (openmetadata-service).

🤖 Generated with Claude Code


Summary by Gitar

  • Change events:
    • Track lastReadOffset in AbstractEventConsumer to ensure cursors advance by true database offset, preventing event redelivery.
  • Search indexer:
    • Added robust exception handling to ElasticSearchBulkSink and OpenSearchBulkSink to prevent concurrent request leaks and scheduler task cancellation.
  • Database utility:
    • Fixed SQL JSON path syntax in ListFilter to correctly evaluate the disabled field for PostgreSQL boolean casting.

This will update automatically on new commits.

Greptile Summary

This PR hardens several backend correctness paths. The main changes are:

  • Guard empty entity pagination pages after concurrent deletes.
  • Check all owners for team-based authorization.
  • Decode pagination cursors with UTF-8.
  • Make app registration and scheduler shutdown safer.
  • Keep search bulk flushing alive across transient failures.
  • Advance change-event polling by the actual read offset.
  • Fix disabled-filter SQL JSON extraction.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/SubjectContext.java Scans the owner list for any team ownership match instead of stopping at the first owner.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java Returns empty result pages safely when cursor pagination races with deleted rows.
openmetadata-service/src/main/java/org/openmetadata/service/util/RestUtil.java Decodes Base64 cursor bytes with UTF-8 to match cursor encoding.
openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/ElasticSearchBulkSink.java Catches scheduled flush failures and releases bulk-send resources on synchronous client errors.
openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/OpenSearchBulkSink.java Catches scheduled flush failures so periodic flushing keeps running.
openmetadata-service/src/main/java/org/openmetadata/service/apps/ApplicationContext.java Uses concurrent app storage for registration and read access.
openmetadata-service/src/main/java/org/openmetadata/service/apps/scheduler/AppScheduler.java Keeps a handle to the error-trigger scheduler and shuts it down with the app scheduler.
openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/changeEvent/AbstractEventConsumer.java Tracks the highest change-event offset read during polling.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ListFilter.java Corrects JSON boolean SQL for disabled filtering.

Reviews (5): Last reviewed commit: "fix: ES bulk-sink sync-throw guard catch..." | Re-trigger Greptile

Context used (3)

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

- EntityRepository.listAfter/listBefore: guard empty result pages so a held
  cursor whose remaining rows were concurrently deleted returns an empty page
  instead of throwing IndexOutOfBounds (HTTP 500).
- SubjectContext.isTeamAsset: evaluate ALL owners rather than returning inside
  the first loop iteration. A multi-owner asset [user, teamB] with a teamB
  policy no longer wrongly denies members of teamB.
- RestUtil.decodeCursor: decode Base64 cursor bytes as UTF-8 to match
  encodeCursor, fixing pagination-cursor corruption for non-ASCII entity names
  on JVMs whose default charset is not UTF-8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 5, 2026 11:26
@github-actions

github-actions Bot commented Jul 5, 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.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 5, 2026
Comment on lines +172 to 185
private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
boolean result = false;
if (owner.getType().equals(Entity.USER)) {
result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
} else if (owner.getType().equals(Entity.TEAM)) {
try {
Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
result = isInTeam(parentTeam, team.getEntityReference());
} catch (Exception ex) {
// Team could not be resolved (e.g. deleted); treat as not under the team.
}
}
return result;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Unresolved User Stops Owner Scan

When a multi-owner asset has an unresolved or deleted user before a valid team owner, this branch lets getSubjectContext(owner.getName()) throw before the loop can check the later owner. matchTeam() then returns an error or denial even though the asset is owned by a matching team, so the new all-owner OR behavior is still skipped for that owner ordering.

Suggested change
private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
boolean result = false;
if (owner.getType().equals(Entity.USER)) {
result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
} else if (owner.getType().equals(Entity.TEAM)) {
try {
Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
result = isInTeam(parentTeam, team.getEntityReference());
} catch (Exception ex) {
// Team could not be resolved (e.g. deleted); treat as not under the team.
}
}
return result;
}
private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
boolean result = false;
if (owner != null && Entity.USER.equals(owner.getType())) {
try {
result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
} catch (Exception ex) {
// User could not be resolved (e.g. deleted); treat as not under the team.
}
} else if (owner != null && Entity.TEAM.equals(owner.getType())) {
try {
Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
result = isInTeam(parentTeam, team.getEntityReference());
} catch (Exception ex) {
// Team could not be resolved (e.g. deleted); treat as not under the team.
}
}
return result;
}

Context Used: CLAUDE.md (source)

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.

Pull request overview

This PR applies three backend correctness hardening fixes across pagination, authorization evaluation for multi-owner assets, and cursor charset handling to prevent server errors and access-denial regressions.

Changes:

  • Hardened keyset pagination cursors in EntityRepository.listAfter/listBefore to avoid cursor computation on empty result pages.
  • Fixed SubjectContext.isTeamAsset to evaluate all owners (OR semantics) instead of returning after the first owner.
  • Made RestUtil.decodeCursor decode Base64 cursor bytes as UTF-8 to match encodeCursor.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
openmetadata-service/src/main/java/org/openmetadata/service/util/RestUtil.java Ensures cursor decoding uses UTF-8 to round-trip non-ASCII cursor values reliably.
openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/SubjectContext.java Corrects team-ownership checks for multi-owner assets by evaluating all owners.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java Adds guards for empty pagination pages to prevent cursor computation exceptions and HTTP 500s.

Comment on lines 2612 to 2616
if (entities.size()
> limitParam) { // If extra result exists, then previous page exists - return before cursor
entities.remove(0);
beforeCursor = getCursorValue(entities.get(0));
beforeCursor = getCursorValue(entities.getFirst());
}
Comment on lines 119 to 123
public static String decodeCursor(String cursor) {
return cursor == null || cursor.isEmpty()
? null
: new String(Base64.getUrlDecoder().decode(cursor));
: new String(Base64.getUrlDecoder().decode(cursor), StandardCharsets.UTF_8);
}
Comment on lines +160 to +169
/** Returns true if any of the resource owners is under the team hierarchy of parentTeam */
public boolean isTeamAsset(String parentTeam, List<EntityReference> owners) {
for (EntityReference owner : owners) {
if (owner.getType().equals(Entity.USER)) {
SubjectContext subjectContext = getSubjectContext(owner.getName());
return subjectContext.isUserUnderTeam(parentTeam);
} else if (owner.getType().equals(Entity.TEAM)) {
try {
Team team =
Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
return isInTeam(parentTeam, team.getEntityReference());
} catch (Exception ex) {
// Ignore and return false
}
boolean isUnderTeam = false;
for (EntityReference owner : listOrEmpty(owners)) {
if (isOwnerUnderTeam(owner, parentTeam)) {
isUnderTeam = true;
break;
}
}
return false;
return isUnderTeam;
Comment on lines +172 to 185
private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
boolean result = false;
if (owner.getType().equals(Entity.USER)) {
result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
} else if (owner.getType().equals(Entity.TEAM)) {
try {
Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
result = isInTeam(parentTeam, team.getEntityReference());
} catch (Exception ex) {
// Team could not be resolved (e.g. deleted); treat as not under the team.
}
}
return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Unresolved user owner aborts multi-owner team check

The intent of this change is to evaluate ALL owners so that a multi-owner asset such as [user, teamB] correctly grants access to teamB members via a matchTeam(teamB) policy. However the USER branch of isOwnerUnderTeam calls getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam) without any try/catch, unlike the TEAM branch which swallows exceptions.

getSubjectContext -> SubjectCache.getUserContext throws (EntityNotFoundException) when the user cannot be resolved — e.g. a deleted/renamed user still referenced as an owner. When the unresolved user is the FIRST owner in the list, that exception propagates out of isOwnerUnderTeam and isTeamAsset entirely, so the later teamB owner is never evaluated. The scan aborts and teamB members are wrongly denied — reintroducing exactly the fail-closed regression this PR set out to fix.

Wrap the USER branch in the same defensive try/catch as the TEAM branch (or handle the exception inside the owner loop) so one unresolvable owner does not short-circuit evaluation of the remaining owners.

Wrap both owner branches so an unresolvable owner does not abort the multi-owner scan.:

private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
  boolean result = false;
  try {
    if (owner.getType().equals(Entity.USER)) {
      result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
    } else if (owner.getType().equals(Entity.TEAM)) {
      Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
      result = isInTeam(parentTeam, team.getEntityReference());
    }
  } catch (Exception ex) {
    // Owner could not be resolved (e.g. deleted user/team); treat as not under the team.
  }
  return result;
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (30 flaky)

✅ 4490 passed · ❌ 0 failed · 🟡 30 flaky · ⏭️ 37 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 436 0 7 16
🟡 Shard 2 816 0 8 8
🟡 Shard 3 799 0 2 7
🟡 Shard 4 811 0 4 5
🟡 Shard 5 861 0 2 0
🟡 Shard 6 767 0 7 1
🟡 30 flaky test(s) (passed on retry)
  • Features/Glossary/GlossaryPagination.spec.ts › should check for nested glossary term search (shard 1, 1 retry)
  • Features/TagsSuggestion.spec.ts › should decline suggested tags for a container column (shard 1, 1 retry)
  • Pages/Roles.spec.ts › Roles page should work properly (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › User without permission (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › User without permission (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a table-scoped user sees tables but never dashboards (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a dashboard-scoped user sees dashboards but never tables (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary Term (Nested) (shard 2, 1 retry)
  • Features/BulkEditOperationBadges.spec.ts › Database service bulk edit search filters rows and clear restores them (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Database service (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Database (shard 2, 1 retry)
  • Features/DataQuality/BundleSuiteBulkOperations.spec.ts › Create new Bundle Suite with bulk selected test cases (shard 2, 1 retry)
  • Features/DataQuality/TestCaseImportExportBasic.spec.ts › should show validation errors for invalid CSV (shard 2, 1 retry)
  • Features/GlobalPageSize.spec.ts › Page size should persist across different pages (shard 2, 1 retry)
  • Features/Glossary/GlossaryP3Tests.spec.ts › should create glossary with unicode characters in name (shard 2, 1 retry)
  • Features/IncidentManager.spec.ts › Resolving incident & re-run pipeline (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 4, 1 retry)
  • Pages/CustomProperties.spec.ts › Should clear search and show all properties for apiCollection in right panel (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for Directory (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Contract Status badge should be visible on condition if Contract Tab is present/hidden by Persona (shard 4, 1 retry)
  • Pages/DomainDataProductsRightPanel.spec.ts › Should edit tags for data product from domain context (shard 5, 1 retry)
  • Pages/ExploreBrowse.spec.ts › service type drill-down disables unrelated roots and query-panel Clear resets it (shard 5, 2 retries)
  • Pages/ExplorePageRightPanel.spec.ts › Should allow Data Steward to edit glossary terms for searchIndex (shard 6, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Glossary Bulk Import Export (shard 6, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Import partial success - some terms pass, some fail (shard 6, 1 retry)
  • Pages/InputOutputPorts.spec.ts › Output ports section collapse/expand (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for pipelineService in platform lineage (shard 6, 1 retry)
  • Pages/UserDetails.spec.ts › Create team with domain and verify visibility of inherited domain in user profile after team removal (shard 6, 1 retry)

📦 Download artifacts

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

mohityadav766 and others added 2 commits July 5, 2026 19:23
…ead leak

- ElasticSearchBulkSink: guard the async bulk() submit like the OpenSearch twin — a
  synchronous throw (dead transport) previously leaked the concurrency permit and the
  activeBulkRequests slot, eventually starving the pipeline and hanging close(). On throw
  we now record the failure and, if no retry is scheduled, decrement + release.
- ApplicationContext: ConcurrentHashMap for the apps map — registerApp() runs on every
  Quartz execution while request/event threads iterate getAllApps()/getApp(), which threw
  ConcurrentModificationException / torn reads on a plain HashMap.
- AppScheduler: retain the error-trigger-reset pool in a field, make its thread a daemon,
  and shutdownNow() it in shutDown(); it was a non-daemon local that survived shutdown and
  blocked JVM exit (and leaked on re-initialize).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ption

scheduleAtFixedRate cancels a periodic task permanently if it throws (ScheduledExecutorService
contract), so an unchecked exception from flushInternal silently disabled periodic flushing —
trailing partial buffers then only shipped on an explicit flush/close. Wrap the flushIfNeeded body
in try/catch so a failure logs and the task keeps running on the next interval (both ES and OS sinks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 5, 2026 14:18

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment on lines 2612 to 2616
if (entities.size()
> limitParam) { // If extra result exists, then previous page exists - return before cursor
entities.remove(0);
beforeCursor = getCursorValue(entities.get(0));
beforeCursor = getCursorValue(entities.getFirst());
}
@mohityadav766 mohityadav766 self-assigned this Jul 5, 2026
mohityadav766 and others added 2 commits July 6, 2026 00:14
…isabled Postgres SQL

- AbstractEventConsumer: advance the subscription cursor to the true maximum change_event.offset
  read (via the offset-returning listWithOffset DAO) instead of by row count. change_event.offset
  is a non-contiguous AUTO_INCREMENT, so advancing by count re-read the tail across gaps (rolled-back
  txns / retention deletes) and redelivered those events (duplicate webhook/alert delivery).
- ListFilter.getDisabledCondition (Postgres branch): fix malformed SQL — ::boolean (not :boolean),
  balanced parentheses, and drop the undefined `c.` alias (use json ->> 'disabled'). The branch was
  dormant (no caller wires a `disabled` param today) but threw a syntax error the moment one did.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…IOException

The permit-leak guard caught IOException, which was never imported (and the ES async client
does not throw a checked IOException synchronously). Catch Exception — handleBulkFailure takes a
Throwable — so a synchronous RuntimeException from the async submit is handled and the permit /
activeBulkRequests slot are released.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Jul 5, 2026

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

Hardens backend pagination, cursor decoding, and search sink reliability. Re-evaluate the multi-owner authorization logic, as the current implementation terminates prematurely and fails to grant access to secondary team owners.

⚠️ Bug: Unresolved user owner aborts multi-owner team check

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/SubjectContext.java:172-185 📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/SubjectContext.java:49-52

The intent of this change is to evaluate ALL owners so that a multi-owner asset such as [user, teamB] correctly grants access to teamB members via a matchTeam(teamB) policy. However the USER branch of isOwnerUnderTeam calls getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam) without any try/catch, unlike the TEAM branch which swallows exceptions.

getSubjectContext -> SubjectCache.getUserContext throws (EntityNotFoundException) when the user cannot be resolved — e.g. a deleted/renamed user still referenced as an owner. When the unresolved user is the FIRST owner in the list, that exception propagates out of isOwnerUnderTeam and isTeamAsset entirely, so the later teamB owner is never evaluated. The scan aborts and teamB members are wrongly denied — reintroducing exactly the fail-closed regression this PR set out to fix.

Wrap the USER branch in the same defensive try/catch as the TEAM branch (or handle the exception inside the owner loop) so one unresolvable owner does not short-circuit evaluation of the remaining owners.

Wrap both owner branches so an unresolvable owner does not abort the multi-owner scan.
private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
  boolean result = false;
  try {
    if (owner.getType().equals(Entity.USER)) {
      result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
    } else if (owner.getType().equals(Entity.TEAM)) {
      Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
      result = isInTeam(parentTeam, team.getEntityReference());
    }
  } catch (Exception ex) {
    // Owner could not be resolved (e.g. deleted user/team); treat as not under the team.
  }
  return result;
}
🤖 Prompt for agents
Code Review: Hardens backend pagination, cursor decoding, and search sink reliability. Re-evaluate the multi-owner authorization logic, as the current implementation terminates prematurely and fails to grant access to secondary team owners.

1. ⚠️ Bug: Unresolved user owner aborts multi-owner team check
   Files: openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/SubjectContext.java:172-185, openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/SubjectContext.java:49-52

   The intent of this change is to evaluate ALL owners so that a multi-owner asset such as `[user, teamB]` correctly grants access to `teamB` members via a `matchTeam(teamB)` policy. However the USER branch of `isOwnerUnderTeam` calls `getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam)` without any try/catch, unlike the TEAM branch which swallows exceptions.
   
   `getSubjectContext` -> `SubjectCache.getUserContext` throws (EntityNotFoundException) when the user cannot be resolved — e.g. a deleted/renamed user still referenced as an owner. When the unresolved user is the FIRST owner in the list, that exception propagates out of `isOwnerUnderTeam` and `isTeamAsset` entirely, so the later `teamB` owner is never evaluated. The scan aborts and teamB members are wrongly denied — reintroducing exactly the fail-closed regression this PR set out to fix.
   
   Wrap the USER branch in the same defensive try/catch as the TEAM branch (or handle the exception inside the owner loop) so one unresolvable owner does not short-circuit evaluation of the remaining owners.

   Fix (Wrap both owner branches so an unresolvable owner does not abort the multi-owner scan.):
   private boolean isOwnerUnderTeam(EntityReference owner, String parentTeam) {
     boolean result = false;
     try {
       if (owner.getType().equals(Entity.USER)) {
         result = getSubjectContext(owner.getName()).isUserUnderTeam(parentTeam);
       } else if (owner.getType().equals(Entity.TEAM)) {
         Team team = Entity.getEntity(Entity.TEAM, owner.getId(), TEAM_FIELDS, Include.NON_DELETED);
         result = isInTeam(parentTeam, team.getEntityReference());
       }
     } catch (Exception ex) {
       // Owner could not be resolved (e.g. deleted user/team); treat as not under the team.
     }
     return result;
   }

Options

Display: compact → Showing less information.

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

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

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