From 3cbdf5d0b7a4aa59aa00cdba274f7734e43a02f7 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:20:59 -0700 Subject: [PATCH] Fix: require view permission for more-like search --- src/documents/tests/test_api_search.py | 52 ++++++++++++++++++++++++++ src/documents/views.py | 25 ++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/documents/tests/test_api_search.py b/src/documents/tests/test_api_search.py index d671fe546..79fae018a 100644 --- a/src/documents/tests/test_api_search.py +++ b/src/documents/tests/test_api_search.py @@ -772,6 +772,58 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): self.assertEqual(results[0]["id"], d3.id) self.assertEqual(results[1]["id"], d1.id) + def test_search_more_like_requires_view_permission_on_seed_document(self): + """ + GIVEN: + - A user can search documents they own + - Another user's private document exists with similar content + WHEN: + - The user requests more-like-this for the private seed document + THEN: + - The request is rejected + """ + owner = User.objects.create_user("owner") + attacker = User.objects.create_user("attacker") + attacker.user_permissions.add( + Permission.objects.get(codename="view_document"), + ) + + private_seed = Document.objects.create( + title="private bank statement", + content="quarterly treasury bank statement wire transfer", + checksum="seed", + owner=owner, + pk=10, + ) + visible_doc = Document.objects.create( + title="attacker-visible match", + content="quarterly treasury bank statement wire transfer summary", + checksum="visible", + owner=attacker, + pk=11, + ) + other_doc = Document.objects.create( + title="unrelated", + content="completely different topic", + checksum="other", + owner=attacker, + pk=12, + ) + + with AsyncWriter(index.open_index()) as writer: + index.update_document(writer, private_seed) + index.update_document(writer, visible_doc) + index.update_document(writer, other_doc) + + self.client.force_authenticate(user=attacker) + + response = self.client.get( + f"/api/documents/?more_like_id={private_seed.id}", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content, b"Insufficient permissions.") + def test_search_filtering(self): t = Tag.objects.create(name="tag") t2 = Tag.objects.create(name="tag2") diff --git a/src/documents/views.py b/src/documents/views.py index e8a929859..5985c17a8 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -49,6 +49,7 @@ from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.timezone import make_aware from django.utils.translation import get_language +from django.utils.translation import gettext_lazy as _ from django.views import View from django.views.decorators.cache import cache_control from django.views.decorators.http import condition @@ -70,6 +71,7 @@ from rest_framework import parsers from rest_framework import serializers from rest_framework.decorators import action from rest_framework.exceptions import NotFound +from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import ValidationError from rest_framework.filters import OrderingFilter from rest_framework.filters import SearchFilter @@ -1369,11 +1371,28 @@ class UnifiedSearchViewSet(DocumentViewSet): filtered_queryset = super().filter_queryset(queryset) if self._is_search_request(): - from documents import index - if "query" in self.request.query_params: + from documents import index + query_class = index.DelayedFullTextQuery elif "more_like_id" in self.request.query_params: + try: + more_like_doc_id = int(self.request.query_params["more_like_id"]) + more_like_doc = Document.objects.select_related("owner").get( + pk=more_like_doc_id, + ) + except (TypeError, ValueError, Document.DoesNotExist): + raise PermissionDenied(_("Invalid more_like_id")) + + if not has_perms_owner_aware( + self.request.user, + "view_document", + more_like_doc, + ): + raise PermissionDenied(_("Insufficient permissions.")) + + from documents import index + query_class = index.DelayedMoreLikeThisQuery else: raise ValueError @@ -1409,6 +1428,8 @@ class UnifiedSearchViewSet(DocumentViewSet): return response except NotFound: raise + except PermissionDenied as e: + return HttpResponseForbidden(str(e.detail)) except Exception as e: logger.warning(f"An error occurred listing search results: {e!s}") return HttpResponseBadRequest(