Fix: authorize document versions by their root in the single-object permission check (#14421)

* Fix: authorize document versions by their root in the single-object permission check

has_perms_owner_aware judged a document by its own owner and grants, so each
endpoint that fetches a document itself had to remember to map a version to
its root document first, and one that forgot, like the more-like-this search
filter, authorized by a stale version owner.

The check now maps a Document to its root before looking at the owner and the
guardian grants, matching what permitted_document_ids does for id sets. The
eight call sites that mapped the document themselves pass it straight through.
The DRF object permission class needs no change because the document viewset
only ever serves root documents.
This commit is contained in:
Trenton H authored and GitHub committed 2026-10-12 17:17:00 +00:00
1 parent 070f614929
commit c2deb69ac5
5 files changed
+136 -9

No files matched your search

+33
View File
@@ -1261,6 +1261,39 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, b"Insufficient permissions.")
def test_search_more_like_version_requires_view_permission_on_its_root(
self,
) -> None:
"""
GIVEN:
- Another user's private root document with an unowned version
WHEN:
- A user without access requests more-like-this for the version
THEN:
- The request is rejected, the version's missing owner grants nothing
"""
owner = UserFactory(username="owner")
attacker = UserFactory(username="attacker")
grant_global(attacker, "view_document")
private_root = DocumentFactory(
title="private bank statement",
content="quarterly treasury bank statement wire transfer",
owner=owner,
)
version = DocumentFactory(
root_document=private_root,
version_index=1,
owner=None,
)
get_backend().add_or_update(private_root)
self.client.force_authenticate(user=attacker)
response = self.client.get(f"/api/documents/?more_like_id={version.id}")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, b"Insufficient permissions.")
def test_search_filtering(self) -> None:
t = Tag.objects.create(name="tag")
t2 = Tag.objects.create(name="tag2")