fix: address PR #813 review round 4 (log leak, cross-user chunk ctx, algo, overlap)
1. Don't log unverified result titles: both search algorithms logged top-5 titles at DEBUG before verify-on-read; with owner-level share expansion the unverified set can contain other users' docs. Algorithms now log a count only; the verifying callers (server/semantic, viz_routes, api/visualization) log verified titles after verify-on-read. 2. Cross-user FILE chunk context: get_chunk_with_context + the Qdrant chunk helpers now take accessible_owners and use build_ownership_filter. For files the expanded scope is honoured only after a per-file file_accessible_by_id check (accessible_owners is owner-level, so the gate prevents a one-file share recipient from reading any of the owner's cached chunks). note/deck/ news stay self-only (per-user APIs) — a documented gap. Both chunk endpoints pass accessible_owners. 3. Algorithm usage: SemanticSearchAlgorithm is not dead (it backs the dense-only option on the viz/API surfaces); added a clarifying comment in server/ semantic.py. Additionally wired accessible_owners + verify-on-read into the /api/v1 search routes (unified_search, vector_search) so the astrolabe surface is ACL-aware too — degrading gracefully to self-only/unverified for non-provisioned callers instead of 401. 4. Overlapping conditions: build_ownership_filter no longer lists self in the owner_id MatchAny branch (self is already covered by the user_id branch); the owner_id branch carries only the OTHER owners. Tests: build_ownership_filter dedup + chunk-bbox filter-shape updates; new ACL-aware get_indexed_doc_types, cached-chunk lookup, and end-to-end cross-user file chunk-context (recipient gets the chunk, non-recipient denied) tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ac041d5c97
commit
8deb48e6fa
@@ -156,11 +156,11 @@ class TestBuildOwnershipFilter:
|
||||
def test_defaults_to_self_only_when_owners_omitted(self) -> None:
|
||||
flt = build_ownership_filter("alice")
|
||||
|
||||
# Self-only: just the user_id branch. Self is NOT duplicated into an
|
||||
# owner_id branch (the user_id branch already covers self-owned content).
|
||||
assert flt.should is not None
|
||||
assert len(flt.should) == 2 # owner_id branch + legacy user_id branch
|
||||
owner_branch, user_branch = flt.should
|
||||
assert owner_branch.key == "owner_id"
|
||||
assert owner_branch.match.any == ["alice"]
|
||||
assert len(flt.should) == 1
|
||||
(user_branch,) = flt.should
|
||||
assert user_branch.key == "user_id"
|
||||
assert user_branch.match.value == "alice"
|
||||
|
||||
@@ -168,10 +168,10 @@ class TestBuildOwnershipFilter:
|
||||
flt = build_ownership_filter("alice", ["alice", "bob", "carol"])
|
||||
|
||||
owner_branch, user_branch = flt.should
|
||||
# Owner branch reflects the expanded set.
|
||||
assert set(owner_branch.match.any) == {"alice", "bob", "carol"}
|
||||
# Legacy user_id branch keeps the original user — that's the only
|
||||
# legacy match path, so it must NOT widen to other owners.
|
||||
# Owner branch holds only the OTHER owners — self ("alice") is excluded
|
||||
# because the user_id branch already matches self-owned content.
|
||||
assert set(owner_branch.match.any) == {"bob", "carol"}
|
||||
assert user_branch.key == "user_id"
|
||||
assert user_branch.match.value == "alice"
|
||||
|
||||
def test_explicit_empty_list_omits_owner_branch_keeps_legacy(self) -> None:
|
||||
|
||||
@@ -68,7 +68,11 @@ class TestIndexedPath:
|
||||
# One scroll call, and the filter must include chunk_index (not offsets)
|
||||
qdrant_client.scroll.assert_awaited_once()
|
||||
scroll_kwargs = qdrant_client.scroll.await_args.kwargs
|
||||
filter_keys = [c.key for c in scroll_kwargs["scroll_filter"].must]
|
||||
# Skip nested Filters (the ACL ownership sub-filter) — only field
|
||||
# conditions carry a `.key`.
|
||||
filter_keys = [
|
||||
c.key for c in scroll_kwargs["scroll_filter"].must if hasattr(c, "key")
|
||||
]
|
||||
assert "chunk_index" in filter_keys
|
||||
assert "chunk_start_offset" not in filter_keys
|
||||
assert "chunk_end_offset" not in filter_keys
|
||||
@@ -92,7 +96,11 @@ class TestOffsetFallbackPath:
|
||||
|
||||
assert result == (bbox, 2)
|
||||
scroll_kwargs = qdrant_client.scroll.await_args.kwargs
|
||||
filter_keys = [c.key for c in scroll_kwargs["scroll_filter"].must]
|
||||
# Skip nested Filters (the ACL ownership sub-filter) — only field
|
||||
# conditions carry a `.key`.
|
||||
filter_keys = [
|
||||
c.key for c in scroll_kwargs["scroll_filter"].must if hasattr(c, "key")
|
||||
]
|
||||
assert "chunk_start_offset" in filter_keys
|
||||
assert "chunk_end_offset" in filter_keys
|
||||
assert "chunk_index" not in filter_keys
|
||||
|
||||
Reference in New Issue
Block a user