From 86846255f41301fbbe71de1c5c60cd26bcceac54 Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:11:19 -0700 Subject: [PATCH] perf: resolve permitted_document_ids(perm=delete_document, include_deleted=True) once for trash loop Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GRp4kf1mdn9ruv81zWAmh2 --- .../test_permission_filtering_security.py | 39 +++++++++++++++++++ src/documents/views.py | 12 ++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/documents/tests/test_permission_filtering_security.py b/src/documents/tests/test_permission_filtering_security.py index 2d701090b..5349e0402 100644 --- a/src/documents/tests/test_permission_filtering_security.py +++ b/src/documents/tests/test_permission_filtering_security.py @@ -351,3 +351,42 @@ class TestBulkDownloadPermissionChecksRootDocument: format="json", ) assert response.status_code == HTTPStatus.FORBIDDEN + + +@pytest.mark.django_db +class TestTrashRestorePermissionBoundary: + def test_restore_rejects_document_without_delete_permission( + self, + rest_api_client, + ): + owner = User.objects.create_user(username="owner") + requester = User.objects.create_user(username="requester") + rest_api_client.force_authenticate(user=requester) + doc = DocumentFactory(owner=owner) + assign_perm("view_document", requester, doc) # view only, NOT delete + doc.delete() + + response = rest_api_client.post( + "/api/trash/", + {"documents": [doc.pk], "action": "restore"}, + format="json", + ) + assert response.status_code == HTTPStatus.FORBIDDEN + + def test_restore_allows_document_with_explicit_delete_permission( + self, + rest_api_client, + ): + owner = User.objects.create_user(username="owner") + requester = User.objects.create_user(username="requester") + rest_api_client.force_authenticate(user=requester) + doc = DocumentFactory(owner=owner) + assign_perm("delete_document", requester, doc) + doc.delete() + + response = rest_api_client.post( + "/api/trash/", + {"documents": [doc.pk], "action": "restore"}, + format="json", + ) + assert response.status_code == HTTPStatus.OK diff --git a/src/documents/views.py b/src/documents/views.py index e8b9d5e8b..e4157028d 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -5308,9 +5308,15 @@ class TrashView(ListModelMixin, PassUserMixin): if doc_ids is not None else self.filter_queryset(self.get_queryset()).all() ) - for doc in docs: - if not has_perms_owner_aware(request.user, "delete_document", doc): - return HttpResponseForbidden("Insufficient permissions") + permitted_ids = set( + permitted_document_ids( + request.user, + perm="delete_document", + include_deleted=True, + ), + ) + if not all(doc.pk in permitted_ids for doc in docs): + return HttpResponseForbidden("Insufficient permissions") action = serializer.validated_data.get("action") if action == "restore": for doc in Document.deleted_objects.filter(id__in=doc_ids).all():