From 9edcfa8682b6add81807b2af6315c24f35070cc8 Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:55:12 -0700 Subject: [PATCH] perf: resolve permitted_document_ids once for root-document version-listing loop BulkDownloadView.post() previously called has_perms_owner_aware() per row inside the loop that resolves each document's root and latest version. Resolve permitted_document_ids(request.user) once before the loop and check membership by root_doc.pk instead, consistent with the other consolidated permission-filtering sites. --- .../test_permission_filtering_security.py | 37 +++++++++++++++++++ src/documents/views.py | 3 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/documents/tests/test_permission_filtering_security.py b/src/documents/tests/test_permission_filtering_security.py index 172bfb197..305566f97 100644 --- a/src/documents/tests/test_permission_filtering_security.py +++ b/src/documents/tests/test_permission_filtering_security.py @@ -1,5 +1,6 @@ from __future__ import annotations +from http import HTTPStatus from unittest.mock import patch import pytest @@ -314,3 +315,39 @@ class TestBulkEditChangePermissionBoundary: format="json", ) assert response.status_code == 403 + + +@pytest.mark.django_db +class TestBulkDownloadPermissionChecksRootDocument: + def test_permission_checked_on_root_not_on_version( + self, + rest_api_client, + paperless_dirs, + _media_settings, + ): + owner = User.objects.create_user(username="owner") + requester = User.objects.create_user(username="requester") + rest_api_client.force_authenticate(user=requester) + root = DocumentFactory(owner=owner) + # a version of root that the requester has NOT been individually granted + version = DocumentFactory(owner=owner, root_document=root, version_index=1) + version.source_path.write_bytes(b"%PDF-1.4 test") + assign_perm("view_document", requester, root) # granted on ROOT only + + response = rest_api_client.post( + "/api/documents/bulk_download/", + {"documents": [version.pk]}, + format="json", + ) + assert ( + response.status_code == HTTPStatus.OK + ) # visible because root is permitted + + stranger = User.objects.create_user(username="mallory") + rest_api_client.force_authenticate(user=stranger) + response = rest_api_client.post( + "/api/documents/bulk_download/", + {"documents": [version.pk]}, + format="json", + ) + assert response.status_code == HTTPStatus.FORBIDDEN diff --git a/src/documents/views.py b/src/documents/views.py index 333b04049..e8b9d5e8b 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -3837,9 +3837,10 @@ class BulkDownloadView(DocumentSelectionMixin, GenericAPIView[Any]): content = serializer.validated_data.get("content") follow_filename_format = serializer.validated_data.get("follow_formatting") + permitted_ids = set(permitted_document_ids(request.user)) for document in documents: root_doc = get_root_document(document) - if not has_perms_owner_aware(request.user, "view_document", root_doc): + if root_doc.pk not in permitted_ids: return HttpResponseForbidden("Insufficient permissions") versioned_documents.append( get_latest_version_for_root(