perf: migrate single-call Document permission sites to permitted_document_ids (#13507)

* perf: migrate 5 single-call Document permission sites to permitted_document_ids

Swaps get_objects_for_user_owner_aware(user, "view_document", Document) for
Document.objects.filter(id__in=permitted_document_ids(user)) at 5 read-only,
single-call sites: AI chat "ask all documents", bulk-edit
_resolve_document_ids all:true branch, SelectionDataView permission check,
global search docs bucket, and the statistics endpoint's Document branch.

Confirmed all 3 callers of _resolve_document_ids always use the default
"view_document" codename before swapping. Added a regression test pinning
the AI-chat owner/permission boundary through the real API client, and
updated 2 existing mocked tests in test_views.py that asserted on
get_objects_for_user_owner_aware for the chat endpoint.

* perf: migrate 3 serialisers.py Document permission sites to permitted_document_ids

Migrates _get_viewable_duplicates(), PaperlessTaskSerializer.get_duplicate_documents(),
and the ShareLinkBundle document field queryset to use permitted_document_ids()
instead of get_objects_for_user_owner_aware()/get_objects_for_user(), consolidating
onto the shared permission-filtering helper. The is_staff gate in
get_duplicate_documents() is preserved as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRp4kf1mdn9ruv81zWAmh2

* refactor: remove dead permission_codename param from _resolve_document_ids

The keyword param was unused in the method body since an earlier commit
switched it to call permitted_document_ids(user) internally. None of the
3 call sites passed it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: drop internal task-number reference from AI chat migration test docstring

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Trenton H
2026-08-04 08:01:24 -07:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 5e54259db9
commit c6cecd3c4e
4 changed files with 99 additions and 58 deletions
+10 -22
View File
@@ -177,6 +177,7 @@ from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import has_global_statistics_permission
from documents.permissions import has_perms_owner_aware
from documents.permissions import has_system_status_permission
from documents.permissions import permitted_document_ids
from documents.permissions import set_permissions_for_object
from documents.plugins.date_parsing import get_date_parser
from documents.schema import generate_object_with_permissions_schema
@@ -2270,10 +2271,8 @@ class ChatStreamingView(GenericAPIView[Any]):
documents = [document]
else:
documents = get_objects_for_user_owner_aware(
request.user,
"view_document",
Document,
documents = Document.objects.filter(
id__in=permitted_document_ids(request.user),
)
output_language = _get_llm_output_language(ai_config=ai_config, request=request)
@@ -2728,7 +2727,6 @@ class DocumentSelectionMixin:
*,
user: User,
validated_data: dict[str, Any],
permission_codename: str = "view_document",
) -> list[int]:
if not validated_data.get("all", False):
# if all is not true, just pass through the provided document ids
@@ -2741,10 +2739,8 @@ class DocumentSelectionMixin:
for key, value in filters.items()
if key not in _TANTIVY_SEARCH_PARAM_NAMES
}
permitted_documents = get_objects_for_user_owner_aware(
user,
permission_codename,
Document,
permitted_documents = Document.objects.filter(
id__in=permitted_document_ids(user),
)
# orm-filtered docs
filtered_documents = DocumentFilterSet(
@@ -3352,10 +3348,8 @@ class SelectionDataView(GenericAPIView[Any]):
serializer.is_valid(raise_exception=True)
ids = serializer.validated_data.get("documents")
permitted_documents = get_objects_for_user_owner_aware(
request.user,
"documents.view_document",
Document,
permitted_documents = Document.objects.filter(
id__in=permitted_document_ids(request.user),
)
if permitted_documents.filter(pk__in=ids).count() != len(ids):
return HttpResponseForbidden("Insufficient permissions")
@@ -3527,10 +3521,8 @@ class GlobalSearchView(PassUserMixin):
OBJECT_LIMIT = 3
docs = []
if request.user.has_perm("documents.view_document"):
all_docs = get_objects_for_user_owner_aware(
request.user,
"view_document",
Document,
all_docs = Document.objects.filter(
id__in=permitted_document_ids(request.user),
)
if db_only:
docs = all_docs.filter(title__icontains=query)[:OBJECT_LIMIT]
@@ -3734,11 +3726,7 @@ class StatisticsView(GenericAPIView[Any]):
documents = (
Document.objects.all()
if can_view_global_stats
else get_objects_for_user_owner_aware(
user,
"documents.view_document",
Document,
)
else Document.objects.filter(id__in=permitted_document_ids(user))
).filter(root_document__isnull=True)
tags = (
Tag.objects.all()