Skip to content

fix(retrieval): enforce workspace file scoping through to Milvus search (#706) - #744

Merged
aditykris merged 1 commit into
developfrom
fix/706-workspace-filter-params-dropped
Jul 22, 2026
Merged

fix(retrieval): enforce workspace file scoping through to Milvus search (#706)#744
aditykris merged 1 commit into
developfrom
fix/706-workspace-filter-params-dropped

Conversation

@aditykris

@aditykris aditykris commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #706 — workspace-scoped search and chat validated the workspace but never actually restricted results to it. filter_params was threaded through every layer of retrieval but silently dropped at VectorStoreSearcher, the one place that builds the Milvus query — so a "workspace search" quietly ran against the whole partition. Root cause and fix details are in the issue.

  • Add a single workspace scope resolver (WorkspaceService.resolve_scope) that resolves workspace_id → (partition, file_ids) via Postgres and fails closed on missing/inaccessible workspaces
  • Fix VectorStoreSearcher.search/multi_query_search to actually merge filter_params into the Milvus query (dense + both legs of hybrid)
  • Narrow the effective search partition to the workspace's own partition, so openrag-all + workspace can't match a same-named file in another partition
  • Scope surrounding/related/ancestor chunk expansion to the same file-id allowlist so it can't reintroduce out-of-workspace documents
  • Replace "log a warning and search unscoped" with a proper WorkspaceNotFoundError (404 for REST search, structured error for both streaming and non-streaming chat)
  • Remove the MCP and by-file-search workarounds that hand-built raw Milvus expressions because filter_params didn't work; both now bind through it properly
  • Fix a real Milvus bug found via integration testing: the empty-file-list short-circuit emitted a bare false literal, which Milvus 2.6 rejects outright — changed to 1 == 0

Test plan

  • uv run pytest tests/unit/ — 1872 passed
  • uv run pytest tests/integration/repos/ against a real Postgres + Milvus stack — all workspace/Milvus-related suites pass (2 pre-existing, unrelated failures in test_partition_repo.py)
  • New end-to-end test (test_workspace_scoping_e2e.py): two partitions, a file sharing the same file_id across both, a workspace scoped to one — confirms cross-partition search returns only the in-scope file, and that removing the file returns zero results rather than falling back to the full partition
  • uv run ruff check / uv run ruff format --check — clean

Summary by CodeRabbit

  • Bug Fixes
    • Workspace-scoped searches now fail closed using an authorization-checked resolved scope, returning consistent 404 behavior when the workspace is unresolved or inaccessible.
    • Related/ancestor/surrounding expansion is now constrained by the same authorized file allowlist, including correct zero results for empty allowlists.
    • File-scoped routes bind file_id using parameterized scoping (no hard-coded filter strings), while preserving any caller filter.
    • Milvus empty-scope filtering now uses an always-false predicate to prevent accidental broad matches.
  • Tests
    • Added end-to-end and unit coverage for workspace scoping, fail-closed empty scopes, and restricted expansion behavior.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a829b039-6e23-44e4-9095-c8e860242dd0

📥 Commits

Reviewing files that changed from the base of the PR and between ea44c8a and 996adde.

📒 Files selected for processing (20)
  • openrag/api/routers/user/search.py
  • openrag/core/models/workspace.py
  • openrag/core/retrieval/pipeline.py
  • openrag/core/retrieval/retriever.py
  • openrag/core/retrieval/searcher.py
  • openrag/core/utils/exceptions.py
  • openrag/services/orchestrators/mcp_service.py
  • openrag/services/orchestrators/query_service.py
  • openrag/services/orchestrators/retrieval_service.py
  • openrag/services/orchestrators/workspace_service.py
  • openrag/services/storage/milvus_store.py
  • openrag/services/storage/vector_store_searcher.py
  • tests/integration/repos/test_workspace_scoping_e2e.py
  • tests/unit/api/routers/user/test_search.py
  • tests/unit/core/retrieval/test_pipeline.py
  • tests/unit/services/orchestrators/test_mcp_service.py
  • tests/unit/services/orchestrators/test_query_service.py
  • tests/unit/services/orchestrators/test_workspace_service.py
  • tests/unit/services/storage/test_milvus_store.py
  • tests/unit/services/storage/test_vector_store_searcher.py
🚧 Files skipped from review as they are similar to previous changes (17)
  • openrag/core/retrieval/pipeline.py
  • tests/unit/services/orchestrators/test_workspace_service.py
  • tests/unit/services/orchestrators/test_mcp_service.py
  • openrag/services/orchestrators/retrieval_service.py
  • openrag/core/utils/exceptions.py
  • openrag/services/storage/milvus_store.py
  • tests/unit/core/retrieval/test_pipeline.py
  • tests/unit/services/storage/test_milvus_store.py
  • openrag/services/orchestrators/workspace_service.py
  • openrag/api/routers/user/search.py
  • openrag/core/retrieval/retriever.py
  • tests/unit/services/orchestrators/test_query_service.py
  • tests/unit/services/storage/test_vector_store_searcher.py
  • openrag/services/storage/vector_store_searcher.py
  • tests/integration/repos/test_workspace_scoping_e2e.py
  • tests/unit/api/routers/user/test_search.py
  • openrag/services/orchestrators/query_service.py

📝 Walkthrough

Walkthrough

Workspace search resolves authorized partitions and file IDs, propagates parameterized restrictions through retrieval expansion, and enforces them in vector-store searches and neighboring-chunk hydration. Empty scopes fail closed, with API, chat, storage, and integration test coverage.

Changes

Workspace-scoped retrieval

Layer / File(s) Summary
Scope resolution and entrypoint wiring
openrag/core/models/workspace.py, openrag/core/utils/exceptions.py, openrag/services/orchestrators/*, openrag/api/routers/user/search.py, tests/unit/api/..., tests/unit/services/orchestrators/*
Workspace IDs resolve to partition and file-ID scopes; search, chat, and MCP pass filter_params, while invalid scopes raise 404 errors.
Filter propagation through expansion
openrag/core/retrieval/*, openrag/services/orchestrators/retrieval_service.py, tests/unit/core/retrieval/test_pipeline.py
Retrieval APIs forward filter parameters into related- and ancestor-chunk expansion and normalize file restrictions into allowlists.
Vector-store allowlist enforcement
openrag/services/storage/*, tests/unit/services/storage/*
Primary, multi-query, surrounding, related, and ancestor searches apply file allowlists; empty lists use the Milvus-compatible 1 == 0 predicate.
Cross-backend workspace validation
tests/integration/repos/test_workspace_scoping_e2e.py
Integration tests verify Postgres scope resolution, partition narrowing, included-file filtering, empty-scope behavior, and unknown-workspace handling against Milvus.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SearchRoute
  participant WorkspaceService
  participant RetrievalService
  participant VectorStoreSearcher
  participant MilvusVectorStore
  Client->>SearchRoute: workspace-scoped search
  SearchRoute->>WorkspaceService: resolve_scope(workspace_id, partitions)
  WorkspaceService-->>SearchRoute: partition and file_ids
  SearchRoute->>RetrievalService: search(filter_params)
  RetrievalService->>VectorStoreSearcher: search with filter_params
  VectorStoreSearcher->>MilvusVectorStore: search scoped filters
  MilvusVectorStore-->>VectorStoreSearcher: matching chunks
  VectorStoreSearcher-->>Client: scoped results
Loading

Possibly related PRs

Suggested labels: bug, fix

Suggested reviewers: hedhoud, enjoybacon7

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enforcing workspace file scoping through Milvus search.
Linked Issues check ✅ Passed The changes implement the issue’s requirements by propagating filter_params into Milvus filtering, resolving workspace scope, and preserving file-level restrictions.
Out of Scope Changes check ✅ Passed The extra changes are still directly tied to workspace scoping, retrieval filtering, or compatibility fixes, with no clear unrelated scope creep.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/706-workspace-filter-params-dropped

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added bug Something isn't working fix Fix issue labels Jul 21, 2026
@Ahmath-Gadji
Ahmath-Gadji force-pushed the fix/706-workspace-filter-params-dropped branch from 7023a25 to 816c418 Compare July 21, 2026 12:52
@aditykris aditykris changed the title fix(retrieval)#706: enforce workspace file scoping through to Milvus search (#706) fix(retrieval): enforce workspace file scoping through to Milvus search (#706) Jul 21, 2026
@aditykris aditykris self-assigned this Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
openrag/services/storage/vector_store_searcher.py (1)

76-77: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Harden filter_params merge against reserved-key collisions. In both search() and multi_query_search(), filters.update(filter_params) runs after filters["partition"] (and optionally filters["expr"]) are set, so a filter_params dict that ever contained a "partition" or "expr" key would silently override the partition-scoping/raw-filter clause — the exact tenant-isolation boundary this PR hardens. No current caller passes those keys (only "file_id" is ever used), so this isn't exploitable today, but a small guard (e.g. reject/ignore reserved keys in filter_params, or assert they're absent) would protect the invariant against future misuse.

  • openrag/services/storage/vector_store_searcher.py#L76-L77: guard against filter_params containing "partition"/"expr" before filters.update(filter_params) in search().
  • openrag/services/storage/vector_store_searcher.py#L107-L108: apply the same guard in multi_query_search().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/storage/vector_store_searcher.py` around lines 76 - 77, The
filter_params merge can overwrite reserved partition-scoping or raw-filter
clauses. In openrag/services/storage/vector_store_searcher.py lines 76-77 within
search(), guard or reject filter_params keys named "partition" and "expr" before
updating filters; apply the identical protection at lines 107-108 within
multi_query_search(), preserving the internally constructed filters.
openrag/api/routers/user/search.py (1)

167-174: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate workspace-scope resolution logic. Both handlers repeat the same "resolve_scope → 404 on None → build filter_params" block; extracting a small helper (e.g. _resolve_workspace_filter_params(workspaces, workspace, allowed_partitions) returning (partitions, filter_params) or raising) would keep the two call sites in sync as the scoping rule evolves.

  • openrag/api/routers/user/search.py#L167-L174: extract the resolve_scope/404/filter_params block from search_multiple_partitions into a shared helper.
  • openrag/api/routers/user/search.py#L256-L259: reuse the same helper in search_one_partition instead of re-implementing the pattern.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/api/routers/user/search.py` around lines 167 - 174, The
workspace-scope resolution and filter construction are duplicated across both
search handlers. In openrag/api/routers/user/search.py lines 167-174, extract
the resolve_scope, 404 handling, partition narrowing, and filter_params
construction into a shared helper; in lines 256-259, replace the duplicate logic
in search_one_partition with that helper, preserving the existing return values
and error behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openrag/api/routers/user/search.py`:
- Around line 167-174: The workspace-scope resolution and filter construction
are duplicated across both search handlers. In
openrag/api/routers/user/search.py lines 167-174, extract the resolve_scope, 404
handling, partition narrowing, and filter_params construction into a shared
helper; in lines 256-259, replace the duplicate logic in search_one_partition
with that helper, preserving the existing return values and error behavior.

In `@openrag/services/storage/vector_store_searcher.py`:
- Around line 76-77: The filter_params merge can overwrite reserved
partition-scoping or raw-filter clauses. In
openrag/services/storage/vector_store_searcher.py lines 76-77 within search(),
guard or reject filter_params keys named "partition" and "expr" before updating
filters; apply the identical protection at lines 107-108 within
multi_query_search(), preserving the internally constructed filters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b7d8d5cc-ffb7-4ed7-9b6f-545a9ceffc78

📥 Commits

Reviewing files that changed from the base of the PR and between b1daecb and 7023a25.

📒 Files selected for processing (20)
  • openrag/api/routers/user/search.py
  • openrag/core/models/workspace.py
  • openrag/core/retrieval/pipeline.py
  • openrag/core/retrieval/retriever.py
  • openrag/core/retrieval/searcher.py
  • openrag/core/utils/exceptions.py
  • openrag/services/orchestrators/mcp_service.py
  • openrag/services/orchestrators/query_service.py
  • openrag/services/orchestrators/retrieval_service.py
  • openrag/services/orchestrators/workspace_service.py
  • openrag/services/storage/milvus_store.py
  • openrag/services/storage/vector_store_searcher.py
  • tests/integration/repos/test_workspace_scoping_e2e.py
  • tests/unit/api/routers/user/test_search.py
  • tests/unit/core/retrieval/test_pipeline.py
  • tests/unit/services/orchestrators/test_mcp_service.py
  • tests/unit/services/orchestrators/test_query_service.py
  • tests/unit/services/orchestrators/test_workspace_service.py
  • tests/unit/services/storage/test_milvus_store.py
  • tests/unit/services/storage/test_vector_store_searcher.py

@aditykris

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot removed the bug Something isn't working label Jul 21, 2026
@hedhoud

hedhoud commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 816c418ce0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@aditykris
aditykris force-pushed the fix/706-workspace-filter-params-dropped branch from 816c418 to ea44c8a Compare July 21, 2026 14:30
@aditykris
aditykris requested a review from hedhoud July 21, 2026 14:31
@coderabbitai coderabbitai Bot added bug Something isn't working and removed fix Fix issue labels Jul 21, 2026

@hedhoud hedhoud left a comment

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.

Reviewed the current workspace scoping changes. The search and expansion paths are covered and look good.

@hedhoud
hedhoud self-requested a review July 22, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace scoping is inert: filter_params is threaded through retrieval and dropped at the vector store

3 participants