perf: resolve permitted_document_ids(perm=change_document) once before bulk-edit loops

Migrates the bulk document edit permission check in views.py and the
custom-field DOCUMENTLINK validator in serialisers.py off of
has_perms_owner_aware-per-document loops, resolving
permitted_document_ids(user, perm="change_document") once instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRp4kf1mdn9ruv81zWAmh2
This commit is contained in:
stumpylog
2026-07-28 07:26:42 -07:00
co-authored by Claude Sonnet 5
parent 6794d6e835
commit 75f8e9f611
3 changed files with 38 additions and 6 deletions
+2 -5
View File
@@ -864,11 +864,8 @@ def validate_documentlink_targets(user, doc_ids):
if user is None:
return
target_documents = Document.objects.filter(id__in=doc_ids).select_related("owner")
if not all(
has_perms_owner_aware(user, "change_document", document)
for document in target_documents
):
permitted_change_ids = set(permitted_document_ids(user, perm="change_document"))
if not set(doc_ids) <= permitted_change_ids:
raise PermissionDenied(
_("Insufficient permissions."),
)
@@ -280,3 +280,37 @@ class TestEmailDocumentPermissionBoundary:
format="json",
)
assert response.status_code == 403
@pytest.mark.django_db
class TestBulkEditChangePermissionBoundary:
def test_bulk_edit_rejects_document_without_change_permission(
self,
rest_api_client,
):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
# grant the global change_document permission so the object-level
# check (not the global has_perm check) is what's under test
requester.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
rest_api_client.force_authenticate(user=requester)
assign_perm(
"view_document",
requester,
DocumentFactory(owner=owner),
) # unrelated grant
target = DocumentFactory(owner=owner)
assign_perm("view_document", requester, target) # view only, NOT change
response = rest_api_client.post(
"/api/documents/bulk_edit/",
{
"documents": [target.pk],
"method": "modify_tags",
"parameters": {"add_tags": [], "remove_tags": []},
},
format="json",
)
assert response.status_code == 403
+2 -1
View File
@@ -2782,8 +2782,9 @@ class DocumentOperationPermissionMixin(PassUserMixin, DocumentSelectionMixin):
)
# check global and object permissions for all documents
permitted_change_ids = set(permitted_document_ids(user, perm="change_document"))
has_perms = user.has_perm("documents.change_document") and all(
has_perms_owner_aware(user, "change_document", doc) for doc in document_objs
doc.pk in permitted_change_ids for doc in document_objs
)
# check ownership for methods that change original document