fix(search): verify shared files by global file id (ACL-aware)
The ACL-aware vector filter (PR #813) expands a user's search to documents whose owner shared them, but verify-on-read still re-checked each file by PATH under the *searching* user's WebDAV root. Nextcloud mounts received shares at the recipient's root by basename, so a nested shared file (e.g. owner's /docs/report.pdf) 404s for the recipient and was silently dropped — defeating the filter for everything but root-level files. Verify files by their global Nextcloud file id instead (the file doc_id IS that id): WebDAVClient.get_file_info_by_id was insufficient (the dav/meta endpoint only resolves the user's own storage, not shares), so add WebDAVClient.file_accessible_by_id which runs a WebDAV SEARCH over the user's whole tree (incl. mounted shares) filtered on oc:fileid. Empirically this resolves owned, directly-shared, and folder-shared files; an empty result is a definitive drop, transport errors are kept as transient. - search/verification.py: _verify_files now checks file_accessible_by_id. - client/webdav.py: add file_accessible_by_id (SEARCH by fileid). - tests/integration/test_acl_owner_filter.py: filter matrix vs real Qdrant. - tests/integration/test_acl_shared_search.py: real-Nextcloud share -> search. - tests/integration/test_verify_on_read.py: nested shared file kept for the recipient; unshared file dropped. - tests/unit/search/test_verification.py: id-based verifier semantics. 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
dc0653c415
commit
bf35200bab
@@ -1073,6 +1073,49 @@ class WebDAVClient(BaseNextcloudClient):
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
async def file_accessible_by_id(self, file_id: int) -> bool:
|
||||
"""ACL-aware access check for a file by its global Nextcloud file ID.
|
||||
|
||||
Used by verify-on-read (ADR-019). Searches the authenticated user's
|
||||
whole files tree — which *includes mounted shares* — via WebDAV SEARCH
|
||||
(RFC 5323) filtered on ``oc:fileid``, returning True iff the user can
|
||||
currently access the file.
|
||||
|
||||
This is the only check that resolves shared files correctly:
|
||||
|
||||
- :meth:`get_file_info` resolves a path under the caller's *own* root,
|
||||
so it 404s on a file shared into the caller's account (Nextcloud
|
||||
mounts received shares at the recipient's root by basename, a
|
||||
different path than the owner indexed).
|
||||
- The ``/remote.php/dav/meta/{id}/`` endpoint resolves only the user's
|
||||
*own* storage, so it 404s on shared files too.
|
||||
|
||||
SEARCH-by-fileid handles all cases: owned files, directly-shared files,
|
||||
and files reachable via a shared parent folder (verified empirically).
|
||||
|
||||
Args:
|
||||
file_id: Nextcloud internal (global) file ID.
|
||||
|
||||
Returns:
|
||||
True if the user can access the file, False if it is not present
|
||||
in their tree (not owned and not shared with them).
|
||||
|
||||
Raises:
|
||||
HTTPStatusError: On transport/server errors — callers treat these
|
||||
as transient (keep the result), not as a definitive denial.
|
||||
"""
|
||||
where = (
|
||||
"<d:eq><d:prop><oc:fileid/></d:prop>"
|
||||
f"<d:literal>{int(file_id)}</d:literal></d:eq>"
|
||||
)
|
||||
results = await self.search_files(
|
||||
scope="", # user's whole files tree, incl. mounted shares
|
||||
where_conditions=where,
|
||||
properties=["fileid"],
|
||||
limit=1,
|
||||
)
|
||||
return len(results) > 0
|
||||
|
||||
async def _get_file_info_by_id(self, file_id: int) -> Dict[str, Any]:
|
||||
"""Get file information by Nextcloud file ID using WebDAV.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user