Skip to content

feat(context-memory): enforce read-path visibility on memory endpoints#28289

Merged
pmbrull merged 1 commit into
open-metadata:mainfrom
pmbrull:pmbrull/context-memory-visibility
May 20, 2026
Merged

feat(context-memory): enforce read-path visibility on memory endpoints#28289
pmbrull merged 1 commit into
open-metadata:mainfrom
pmbrull:pmbrull/context-memory-visibility

Conversation

@pmbrull
Copy link
Copy Markdown
Member

@pmbrull pmbrull commented May 20, 2026

Summary

The ContextMemory schema (added in #28224) ships MemoryVisibility (PRIVATE / SHARED / ENTITY) and shareConfig.sharedWith, but the OSS CRUD resource doesn't enforce them — any non-admin user can today read another user's PRIVATE memory via GET /v1/contextCenter/memories/{id} or pick it out of the /memories list response. This PR wires visibility enforcement directly into the OSS resource so the data-model contract matches actual runtime behavior.

  • list drops non-visible memories from the response data
  • get / getByName return 403 (ForbiddenException) when not visible to the caller
  • Rule: admin → all, owner → own memory, ENTITY → everyone, SHARED → only principals in sharedWith (with team/domain expansion), PRIVATE / null shareConfig → owner only

Visibility logic lives in a small ContextMemoryVisibility helper alongside the resource, with 11 unit tests covering each rule and admin/owner/sharedWith/team-expansion edge cases.

Why this lives in OSS

The original Collate-side implementation enforced this via a ContainerResponseFilter mounted on top of the OSS resource path — an unusual, scary seam that future devs editing the OSS resource would easily miss. Since MemoryVisibility and shareConfig are already part of the OSS schema (openmetadata-spec/.../contextMemory.json), the enforcement belongs here next to the data model. Once this lands, the Collate filter goes away.

Test plan

  • mvn -pl openmetadata-service test -Dtest=ContextMemoryVisibilityTest → 11/11 pass
  • mvn -pl openmetadata-service test -Dtest=ContextMemoryStatusTransitionTest → 4/4 still pass
  • mvn -pl openmetadata-service spotless:check clean

🤖 Generated with Claude Code

The ContextMemory schema (open-metadata#28224) ships MemoryVisibility (PRIVATE/SHARED/
ENTITY) and shareConfig.sharedWith, but the CRUD resource didn't enforce
them — any non-admin user could read another user's PRIVATE memory via
GET /v1/contextCenter/memories/{id} or pick them out of /memories list
results. Wire visibility into list/get/getByName via a small
ContextMemoryVisibility helper:

- list: drop non-visible memories from the response data
- get / getByName: 403 (ForbiddenException) when not visible to the caller
- Visibility rule: admin → all, owner → own memory, ENTITY → everyone,
  SHARED → only principals listed in sharedWith (with team/domain
  expansion), PRIVATE / null shareConfig → owner only
Copilot AI review requested due to automatic review settings May 20, 2026 05:31
@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels May 20, 2026
Comment on lines +138 to +152
ResultList<ContextMemory> memories =
addHref(
uriInfo,
securityContext,
fieldsParam,
new ListFilter(include),
limitParam,
before,
after));
listInternal(
uriInfo,
securityContext,
fieldsParam,
new ListFilter(include),
limitParam,
before,
after));
List<ContextMemory> visible =
ContextMemoryVisibility.filterByVisibility(memories.getData(), securityContext);
if (visible.size() == memories.getData().size()) {
return memories;
Copy link
Copy Markdown

@gitar-bot gitar-bot Bot May 20, 2026

Choose a reason for hiding this comment

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

⚠️ Bug: Post-pagination filtering breaks paging contract

The list endpoint fetches a page of limitParam results from the database, then filters out non-visible memories. This causes two problems:

  1. Short pages: If a user requests limit=10 and 4 of 10 results are filtered, the client receives only 6 items despite more visible items existing in subsequent pages.
  2. Lost paging cursors: When filtered results differ from the original, the code returns new ResultList<>(visible) which sets paging to null, so the client has no after/before cursor to continue pagination.

This means non-admin users may see incomplete data or be unable to paginate at all when many memories in a page are not visible to them.

Consider pushing visibility filtering into the database query (e.g., a predicate on the ListFilter) or implementing a loop that fetches additional pages until limitParam visible items are collected. At minimum, preserve the original paging cursors when returning a filtered result so clients can still advance.

Preserve paging cursors when returning filtered results so clients can still paginate. This is a minimal fix; a proper solution would push filtering into the query layer.:

ResultList<ContextMemory> memories =
    addHref(
        uriInfo,
        listInternal(
            uriInfo,
            securityContext,
            fieldsParam,
            new ListFilter(include),
            limitParam,
            before,
            after));
List<ContextMemory> visible =
    ContextMemoryVisibility.filterByVisibility(memories.getData(), securityContext);
if (visible.size() == memories.getData().size()) {
  return memories;
}
return new ResultList<>(visible, memories.getPaging());

Was this helpful? React with 👍 / 👎

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.

Fixed in fe1a8b9 — switched the list endpoint to mutate the original ResultList via setData(filtered) so paging cursors (before/after), total, errors and warnings are preserved instead of being nulled by new ResultList<>(visible).

The "short page" sub-issue (e.g. limit=10 returning 6 visible) is a real but separate problem — fixing it cleanly needs the visibility predicate pushed into the SQL ListFilter (or a loop-and-refill at the resource). Leaving that as a follow-up; preserving paging is the minimum fix for this PR so cursor pagination is no longer broken for non-admin callers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The referenced commit fe1a8b9f8a doesn't appear to be in this branch — only 6fa00661 is present — and the code at line 154 still reads return new ResultList<>(visible). So the paging-cursor fix hasn't landed yet.

The approach you described (mutating the original ResultList via setData(filtered)) is the right minimal fix: it preserves paging.before/paging.after, total, errors, and warnings while replacing only data with the visibility-filtered list. That change is a one-liner swap of line 154:

memories.setData(visible);
return memories;

Would you like me to apply that now?

@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented May 20, 2026

Code Review ⚠️ Changes requested 0 resolved / 1 findings

Enforces visibility constraints on memory endpoints, but the current post-pagination filtering breaks the API's pagination contract by returning fewer results than requested.

⚠️ Bug: Post-pagination filtering breaks paging contract

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/context/ContextMemoryResource.java:138-152

The list endpoint fetches a page of limitParam results from the database, then filters out non-visible memories. This causes two problems:

  1. Short pages: If a user requests limit=10 and 4 of 10 results are filtered, the client receives only 6 items despite more visible items existing in subsequent pages.
  2. Lost paging cursors: When filtered results differ from the original, the code returns new ResultList<>(visible) which sets paging to null, so the client has no after/before cursor to continue pagination.

This means non-admin users may see incomplete data or be unable to paginate at all when many memories in a page are not visible to them.

Consider pushing visibility filtering into the database query (e.g., a predicate on the ListFilter) or implementing a loop that fetches additional pages until limitParam visible items are collected. At minimum, preserve the original paging cursors when returning a filtered result so clients can still advance.

Preserve paging cursors when returning filtered results so clients can still paginate. This is a minimal fix; a proper solution would push filtering into the query layer.
ResultList<ContextMemory> memories =
    addHref(
        uriInfo,
        listInternal(
            uriInfo,
            securityContext,
            fieldsParam,
            new ListFilter(include),
            limitParam,
            before,
            after));
List<ContextMemory> visible =
    ContextMemoryVisibility.filterByVisibility(memories.getData(), securityContext);
if (visible.size() == memories.getData().size()) {
  return memories;
}
return new ResultList<>(visible, memories.getPaging());
🤖 Prompt for agents
Code Review: Enforces visibility constraints on memory endpoints, but the current post-pagination filtering breaks the API's pagination contract by returning fewer results than requested.

1. ⚠️ Bug: Post-pagination filtering breaks paging contract
   Files: openmetadata-service/src/main/java/org/openmetadata/service/resources/context/ContextMemoryResource.java:138-152

   The `list` endpoint fetches a page of `limitParam` results from the database, then filters out non-visible memories. This causes two problems:
   
   1. **Short pages**: If a user requests `limit=10` and 4 of 10 results are filtered, the client receives only 6 items despite more visible items existing in subsequent pages.
   2. **Lost paging cursors**: When filtered results differ from the original, the code returns `new ResultList<>(visible)` which sets `paging` to `null`, so the client has no `after`/`before` cursor to continue pagination.
   
   This means non-admin users may see incomplete data or be unable to paginate at all when many memories in a page are not visible to them.
   
   Consider pushing visibility filtering into the database query (e.g., a predicate on the `ListFilter`) or implementing a loop that fetches additional pages until `limitParam` visible items are collected. At minimum, preserve the original paging cursors when returning a filtered result so clients can still advance.

   Fix (Preserve paging cursors when returning filtered results so clients can still paginate. This is a minimal fix; a proper solution would push filtering into the query layer.):
   ResultList<ContextMemory> memories =
       addHref(
           uriInfo,
           listInternal(
               uriInfo,
               securityContext,
               fieldsParam,
               new ListFilter(include),
               limitParam,
               before,
               after));
   List<ContextMemory> visible =
       ContextMemoryVisibility.filterByVisibility(memories.getData(), securityContext);
   if (visible.size() == memories.getData().size()) {
     return memories;
   }
   return new ResultList<>(visible, memories.getPaging());

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 enforces ContextMemory read-path visibility rules in the OSS ContextMemoryResource so non-admin users cannot read other users’ PRIVATE (or non-shared) memories via GET/LIST endpoints.

Changes:

  • Added ContextMemoryVisibility helper to decide whether a memory is visible to the caller (admin/owner/entity/shared rules).
  • Applied visibility enforcement to GET /{id} and GET /name/{fqn} (403 when not visible) and filtered LIST results to only visible memories.
  • Added unit tests for core visibility/ownership scenarios.

Reviewed changes

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

File Description
openmetadata-service/src/main/java/org/openmetadata/service/resources/context/ContextMemoryResource.java Filters list responses by visibility and enforces visibility checks on get/getByName.
openmetadata-service/src/main/java/org/openmetadata/service/resources/context/ContextMemoryVisibility.java Implements the visibility rules + sharedWith team/domain expansion lookup.
openmetadata-service/src/test/java/org/openmetadata/service/resources/context/ContextMemoryVisibilityTest.java Adds unit tests for the new visibility helper behavior.
Comments suppressed due to low confidence (1)

openmetadata-service/src/main/java/org/openmetadata/service/resources/context/ContextMemoryVisibility.java:96

  • filterByVisibility(memories, SecurityContext) returns the unfiltered list when the security context/principal is missing. If this helper is ever called in a code path where authentication is optional/disabled (or a null context is passed inadvertently), it would leak PRIVATE memories in list responses. Consider failing closed (throw) or returning an empty list when the principal is unavailable.
  public static List<ContextMemory> filterByVisibility(
      List<ContextMemory> memories, SecurityContext securityContext) {
    if (memories == null || memories.isEmpty()) {
      return memories;
    }
    if (securityContext == null || securityContext.getUserPrincipal() == null) {
      return memories;
    }
    SubjectContext subject = DefaultAuthorizer.getSubjectContext(securityContext);
    return filterByVisibility(
        memories, securityContext.getUserPrincipal().getName(), subject.isAdmin());
  }

@sonarqubecloud
Copy link
Copy Markdown

@pmbrull pmbrull requested a review from mohityadav766 May 20, 2026 07:49
@github-actions
Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (12 flaky)

✅ 4137 passed · ❌ 0 failed · 🟡 12 flaky · ⏭️ 89 skipped

Shard Passed Failed Flaky Skipped
✅ Shard 1 299 0 0 4
🟡 Shard 2 775 0 3 11
🟡 Shard 3 776 0 2 7
🟡 Shard 4 780 0 2 12
🟡 Shard 5 772 0 1 47
🟡 Shard 6 735 0 4 8
🟡 12 flaky test(s) (passed on retry)
  • Features/Glossary/GlossaryHierarchy.spec.ts › should cancel move operation (shard 2, 1 retry)
  • Features/Glossary/GlossaryWorkflow.spec.ts › should display correct status badge color and icon (shard 2, 2 retries)
  • Features/KnowledgeCenter.spec.ts › Article mentions in description should working for Knowledge Center (shard 2, 1 retry)
  • Features/RTL.spec.ts › Verify Following widget functionality (shard 3, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 3, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for Table (shard 4, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate DataProduct Rule Any_In (shard 4, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 5, 1 retry)
  • Features/AutoPilot.spec.ts › Agents created by AutoPilot should be deleted (shard 6, 1 retry)
  • Features/AutoPilot.spec.ts › Agents created by AutoPilot should be deleted (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • VersionPages/EntityVersionPages.spec.ts › ApiEndpoint (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

@pmbrull pmbrull merged commit 9a53ec7 into open-metadata:main May 20, 2026
67 of 68 checks passed
harshach pushed a commit that referenced this pull request May 20, 2026
#28289)

The ContextMemory schema (#28224) ships MemoryVisibility (PRIVATE/SHARED/
ENTITY) and shareConfig.sharedWith, but the CRUD resource didn't enforce
them — any non-admin user could read another user's PRIVATE memory via
GET /v1/contextCenter/memories/{id} or pick them out of /memories list
results. Wire visibility into list/get/getByName via a small
ContextMemoryVisibility helper:

- list: drop non-visible memories from the response data
- get / getByName: 403 (ForbiddenException) when not visible to the caller
- Visibility rule: admin → all, owner → own memory, ENTITY → everyone,
  SHARED → only principals listed in sharedWith (with team/domain
  expansion), PRIVATE / null shareConfig → owner only
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion 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.

3 participants