fix: address PR #813 latest review (ACL-aware doc-type discovery, robustness)

- get_indexed_doc_types: add optional accessible_owners param and reuse
  build_ownership_filter so cross-user doc-type discovery matches the real
  search scope (was self-only / ACL-blind); docstring documents the self-only
  default. Covered by test_get_indexed_doc_types_is_acl_aware.
- access_filter: build_ownership_filter now omits the owner_id branch entirely
  for an empty owner set instead of relying on undocumented MatchAny(any=[])
  semantics; updated the empty-list unit test accordingly.
- access_filter: make the uid_owner/owner share-owner extraction explicit
  ("absent, not empty") to avoid skipping on a falsy-but-present field.
- access_filter: add an operator note that pre-owner_id points need a re-index
  to surface to share recipients (ACL search is a no-op for legacy data).
- verification/webdav: lock the file_accessible_by_id(scope="") contract with a
  targeted multi-user test (owner + recipient True, non-recipient False).
- viz_routes: comment that verify-on-read eviction runs inline by design (no
  lifespan task group available on the Starlette route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-29 14:57:34 +02:00
co-authored by Claude Opus 4.8
parent 531228d407
commit 423d0a1758
6 changed files with 99 additions and 27 deletions
@@ -23,6 +23,7 @@ from qdrant_client.models import Distance, PointStruct, VectorParams
from nextcloud_mcp_server.config import get_settings
from nextcloud_mcp_server.embedding import SimpleEmbeddingProvider
from nextcloud_mcp_server.search.algorithms import get_indexed_doc_types
from nextcloud_mcp_server.search.semantic import SemanticSearchAlgorithm
pytestmark = pytest.mark.integration
@@ -92,6 +93,11 @@ async def seeded_collection(monkeypatch):
"nextcloud_mcp_server.search.semantic.get_embedding_service",
lambda: provider,
)
# get_indexed_doc_types reads the client from the algorithms module.
monkeypatch.setattr(
"nextcloud_mcp_server.search.algorithms.get_qdrant_client",
AsyncMock(return_value=client),
)
yield provider
@@ -170,3 +176,16 @@ async def test_owner_sees_own_new_style_point(seeded_collection):
assert "101" in found
assert "102" not in found
assert "103" not in found
async def test_get_indexed_doc_types_is_acl_aware(seeded_collection):
"""get_indexed_doc_types respects the ownership scope: with the expanded
accessible_owners Bob discovers the shared "file" type, but self-only Bob
(who owns nothing here) discovers nothing — proving it is no longer
ACL-blind."""
# ACL-aware: Bob can read Alice's shared file → discovers "file".
assert await get_indexed_doc_types("bob", accessible_owners=["bob", "alice"]) == {
"file"
}
# Self-only (default): Bob owns nothing here → discovers nothing.
assert await get_indexed_doc_types("bob") == set()
@@ -195,3 +195,23 @@ async def test_non_recipient_does_not_find_file(acl_users, seeded_semantic):
kept = await _search_as(acl_users["diana"], seeded_semantic)
assert kept == [], "diana (no share) must not find alice's file"
async def test_file_accessible_by_id_resolves_shares(acl_users, shared_file):
"""Lock the verify-on-read contract directly on ``file_accessible_by_id``.
The WebDAV SEARCH-by-fileid with ``scope=""`` must resolve a file that the
caller does NOT own but which is shared with them. This is the exact check
verify-on-read depends on for shared, nested files; a Nextcloud change to
how ``scope=""`` is interpreted would otherwise silently break ACL-aware
verification. The file lives in a subfolder, so a path-based check would
404 for the recipient — only the by-id SEARCH gets it right.
"""
file_id, _path = shared_file
fid = int(file_id)
# Owner and share recipient can both reach it...
assert await acl_users["alice"].webdav.file_accessible_by_id(fid) is True
assert await acl_users["bob"].webdav.file_accessible_by_id(fid) is True
# ...the non-recipient cannot.
assert await acl_users["diana"].webdav.file_accessible_by_id(fid) is False