mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-08 09:07:13 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9821e4f73c | ||
|
|
31776986e5 | ||
|
|
94ae632920 | ||
|
|
56d5bb7255 | ||
|
|
2df81ce44e |
No files matched your search
@@ -90,6 +90,7 @@ from documents.templating.utils import convert_format_str_to_template_format
|
|||||||
from documents.templating.workflows import validate_workflow_template
|
from documents.templating.workflows import validate_workflow_template
|
||||||
from documents.validators import uri_validator
|
from documents.validators import uri_validator
|
||||||
from documents.validators import url_validator
|
from documents.validators import url_validator
|
||||||
|
from documents.versioning import get_root_document
|
||||||
from documents.versioning import has_prefetched_effective_content
|
from documents.versioning import has_prefetched_effective_content
|
||||||
from documents.versioning import sort_versions_newest_first
|
from documents.versioning import sort_versions_newest_first
|
||||||
|
|
||||||
@@ -2894,7 +2895,7 @@ class ShareLinkSerializer(OwnedObjectSerializer):
|
|||||||
and has_perms_owner_aware(
|
and has_perms_owner_aware(
|
||||||
self.user,
|
self.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
document,
|
get_root_document(document),
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return document
|
return document
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from auditlog.models import LogEntry # type: ignore[import-untyped]
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase as DjangoTestCase
|
from django.test import TestCase as DjangoTestCase
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
@@ -16,13 +17,17 @@ from documents.data_models import DocumentSource
|
|||||||
from documents.filters import EffectiveContentFilter
|
from documents.filters import EffectiveContentFilter
|
||||||
from documents.filters import TitleContentFilter
|
from documents.filters import TitleContentFilter
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from documents.models import Note
|
||||||
|
from documents.models import ShareLink
|
||||||
from documents.versioning import annotate_effective_content
|
from documents.versioning import annotate_effective_content
|
||||||
from documents.views import DocumentSelectionMixin
|
from documents.views import DocumentSelectionMixin
|
||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from paperless_testing.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.factories import UserFactory
|
from paperless_testing.factories import UserFactory
|
||||||
from paperless_testing.http import read_streaming_response
|
from paperless_testing.http import read_streaming_response
|
||||||
|
from paperless_testing.permissions import grant_all_global
|
||||||
from paperless_testing.permissions import grant_global
|
from paperless_testing.permissions import grant_global
|
||||||
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -1043,3 +1048,128 @@ class TestBulkSelectionExcludesVersions(DjangoTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(selected, [root.id])
|
self.assertEqual(selected, [root.id])
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionActionPermissions(DirectoriesMixin, APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user = UserFactory()
|
||||||
|
grant_all_global(self.user)
|
||||||
|
self.client.force_authenticate(self.user)
|
||||||
|
self.root = DocumentFactory(owner=UserFactory())
|
||||||
|
self.version = DocumentFactory(root_document=self.root, owner=None)
|
||||||
|
|
||||||
|
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||||
|
def test_actions_reject_stale_version_ownership(self):
|
||||||
|
note = Note.objects.create(document=self.version, note="Version note")
|
||||||
|
for owner in (None, self.user):
|
||||||
|
self.version.owner = owner
|
||||||
|
self.version.save(update_fields=["owner"])
|
||||||
|
for action in (
|
||||||
|
"notes",
|
||||||
|
"suggestions",
|
||||||
|
"ai_suggestions",
|
||||||
|
"history",
|
||||||
|
"share_links",
|
||||||
|
):
|
||||||
|
with self.subTest(owner=owner, action=action):
|
||||||
|
response = self.client.get(
|
||||||
|
f"/api/documents/{self.version.pk}/{action}/",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
response = self.client.post(
|
||||||
|
f"/api/documents/{self.version.pk}/notes/",
|
||||||
|
{"note": "New note"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
response = self.client.delete(
|
||||||
|
f"/api/documents/{self.version.pk}/notes/?id={note.pk}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/share_links/",
|
||||||
|
{"document": self.version.pk, "file_version": "original"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/share_link_bundles/",
|
||||||
|
{"document_ids": [self.version.pk], "file_version": "original"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/email/",
|
||||||
|
{
|
||||||
|
"documents": [self.version.pk],
|
||||||
|
"addresses": "recipient@example.com",
|
||||||
|
"subject": "Version",
|
||||||
|
"message": "Version",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
with (
|
||||||
|
mock.patch("documents.views.AIConfig") as ai_config,
|
||||||
|
mock.patch("documents.views.stream_chat_with_documents") as chat,
|
||||||
|
):
|
||||||
|
ai_config.return_value.ai_enabled = True
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/chat/",
|
||||||
|
{"q": "Version?", "document_id": self.version.pk},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
chat.assert_not_called()
|
||||||
|
self.assertTrue(Note.objects.filter(pk=note.pk).exists())
|
||||||
|
self.assertFalse(ShareLink.objects.exists())
|
||||||
|
|
||||||
|
@mock.patch("documents.views.build_share_link_bundle.apply_async")
|
||||||
|
def test_root_permissions_allow_sharing_a_private_version(self, build_mock):
|
||||||
|
self.version.owner = UserFactory()
|
||||||
|
self.version.save(update_fields=["owner"])
|
||||||
|
grant_object(self.user, self.root, "view_document", "change_document")
|
||||||
|
note = Note.objects.create(document=self.version, note="Version note")
|
||||||
|
response = self.client.get(f"/api/documents/{self.version.pk}/notes/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data[0]["id"], note.pk)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/share_links/",
|
||||||
|
{"document": self.version.pk, "file_version": "original"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(ShareLink.objects.get().document_id, self.version.pk)
|
||||||
|
response = self.client.get(f"/api/documents/{self.version.pk}/share_links/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/share_link_bundles/",
|
||||||
|
{"document_ids": [self.version.pk], "file_version": "original"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
build_mock.assert_called_once()
|
||||||
|
|
||||||
|
def test_root_view_permission_does_not_allow_note_changes(self):
|
||||||
|
grant_object(self.user, self.root, "view_document")
|
||||||
|
note = Note.objects.create(document=self.version, note="Version note")
|
||||||
|
response = self.client.get(f"/api/documents/{self.version.pk}/notes/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.post(
|
||||||
|
f"/api/documents/{self.version.pk}/notes/",
|
||||||
|
{"note": "New note"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
response = self.client.delete(
|
||||||
|
f"/api/documents/{self.version.pk}/notes/?id={note.pk}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(Note.objects.filter(pk=note.pk).exists())
|
||||||
|
|
||||||
|
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||||
|
def test_history_uses_root_ownership(self):
|
||||||
|
self.root.owner = self.user
|
||||||
|
self.root.save(update_fields=["owner"])
|
||||||
|
self.version.owner = UserFactory()
|
||||||
|
self.version.save(update_fields=["owner"])
|
||||||
|
response = self.client.get(f"/api/documents/{self.version.pk}/history/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
@@ -279,3 +279,71 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
|
|||||||
Document.objects.filter(root_document=root).values_list("id", flat=True),
|
Document.objects.filter(root_document=root).values_list("id", flat=True),
|
||||||
[version.pk for version in versions],
|
[version.pk for version in versions],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_api_trash_version_follows_root_owner(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A deleted version of user2's document, owned by nobody
|
||||||
|
- A deleted version of the user's document, owned by user2
|
||||||
|
WHEN:
|
||||||
|
- The user lists the trash and tries to restore or empty the versions
|
||||||
|
THEN:
|
||||||
|
- Only the version of the user's own document is listed
|
||||||
|
- The other version can't be restored or emptied
|
||||||
|
- The version of the user's own document can be restored
|
||||||
|
"""
|
||||||
|
user2 = UserFactory(username="user2")
|
||||||
|
other_root = Document.objects.create(
|
||||||
|
title="other root",
|
||||||
|
checksum="other-root",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
owner=user2,
|
||||||
|
)
|
||||||
|
other_version = Document.objects.create(
|
||||||
|
title="other version",
|
||||||
|
checksum="other-version",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
root_document=other_root,
|
||||||
|
version_index=1,
|
||||||
|
)
|
||||||
|
other_version.delete()
|
||||||
|
own_root = Document.objects.create(
|
||||||
|
title="own root",
|
||||||
|
checksum="own-root",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
owner=self.user,
|
||||||
|
)
|
||||||
|
own_version = Document.objects.create(
|
||||||
|
title="own version",
|
||||||
|
checksum="own-version",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
owner=user2,
|
||||||
|
root_document=own_root,
|
||||||
|
version_index=1,
|
||||||
|
)
|
||||||
|
own_version.delete()
|
||||||
|
|
||||||
|
resp = self.client.get("/api/trash/")
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
[doc["id"] for doc in resp.data["results"]],
|
||||||
|
[own_version.pk],
|
||||||
|
)
|
||||||
|
|
||||||
|
for action in ("restore", "empty"):
|
||||||
|
with self.subTest(action=action):
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/trash/",
|
||||||
|
{"action": action, "documents": [other_version.pk]},
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertTrue(
|
||||||
|
Document.deleted_objects.filter(pk=other_version.pk).exists(),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/trash/",
|
||||||
|
{"action": "restore", "documents": [own_version.pk]},
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertTrue(Document.objects.filter(pk=own_version.pk).exists())
|
||||||
+69
-27
@@ -1550,13 +1550,16 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
def suggestions(self, request, pk=None):
|
def suggestions(self, request, pk=None):
|
||||||
doc = get_object_or_404(
|
doc = get_object_or_404(
|
||||||
Document.objects.select_related("owner").prefetch_related("versions"),
|
Document.objects.select_related(
|
||||||
|
"owner",
|
||||||
|
"root_document__owner",
|
||||||
|
).prefetch_related("versions"),
|
||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"change_document",
|
"change_document",
|
||||||
doc,
|
get_root_document(doc),
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
@@ -1610,13 +1613,16 @@ class DocumentViewSet(
|
|||||||
@method_decorator(cache_control(no_cache=True))
|
@method_decorator(cache_control(no_cache=True))
|
||||||
def ai_suggestions(self, request, pk=None):
|
def ai_suggestions(self, request, pk=None):
|
||||||
doc = get_object_or_404(
|
doc = get_object_or_404(
|
||||||
Document.objects.select_related("owner").prefetch_related("versions"),
|
Document.objects.select_related(
|
||||||
|
"owner",
|
||||||
|
"root_document__owner",
|
||||||
|
).prefetch_related("versions"),
|
||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"change_document",
|
"change_document",
|
||||||
doc,
|
get_root_document(doc),
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
@@ -1856,15 +1862,20 @@ class DocumentViewSet(
|
|||||||
currentUser = request.user
|
currentUser = request.user
|
||||||
try:
|
try:
|
||||||
doc = (
|
doc = (
|
||||||
Document.objects.select_related("owner")
|
Document.objects.select_related("owner", "root_document__owner")
|
||||||
.prefetch_related("notes")
|
.prefetch_related("notes")
|
||||||
.only("pk", "owner__id")
|
.only(
|
||||||
|
"pk",
|
||||||
|
"owner__id",
|
||||||
|
"root_document__id",
|
||||||
|
"root_document__owner__id",
|
||||||
|
)
|
||||||
.get(pk=pk)
|
.get(pk=pk)
|
||||||
)
|
)
|
||||||
if currentUser is not None and not has_perms_owner_aware(
|
if currentUser is not None and not has_perms_owner_aware(
|
||||||
currentUser,
|
currentUser,
|
||||||
"view_document",
|
"view_document",
|
||||||
doc,
|
get_root_document(doc),
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions to view notes")
|
return HttpResponseForbidden("Insufficient permissions to view notes")
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
@@ -1886,7 +1897,7 @@ class DocumentViewSet(
|
|||||||
if currentUser is not None and not has_perms_owner_aware(
|
if currentUser is not None and not has_perms_owner_aware(
|
||||||
currentUser,
|
currentUser,
|
||||||
"change_document",
|
"change_document",
|
||||||
doc,
|
get_root_document(doc),
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden(
|
||||||
"Insufficient permissions to create notes",
|
"Insufficient permissions to create notes",
|
||||||
@@ -1929,7 +1940,7 @@ class DocumentViewSet(
|
|||||||
if currentUser is not None and not has_perms_owner_aware(
|
if currentUser is not None and not has_perms_owner_aware(
|
||||||
currentUser,
|
currentUser,
|
||||||
"change_document",
|
"change_document",
|
||||||
doc,
|
get_root_document(doc),
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions to delete notes")
|
return HttpResponseForbidden("Insufficient permissions to delete notes")
|
||||||
|
|
||||||
@@ -1973,11 +1984,13 @@ class DocumentViewSet(
|
|||||||
def share_links(self, request, pk=None):
|
def share_links(self, request, pk=None):
|
||||||
currentUser = request.user
|
currentUser = request.user
|
||||||
try:
|
try:
|
||||||
doc = Document.objects.select_related("owner").get(pk=pk)
|
doc = Document.objects.select_related("owner", "root_document__owner").get(
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
if currentUser is not None and not has_perms_owner_aware(
|
if currentUser is not None and not has_perms_owner_aware(
|
||||||
currentUser,
|
currentUser,
|
||||||
"change_document",
|
"change_document",
|
||||||
doc,
|
get_root_document(doc),
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden(
|
||||||
"Insufficient permissions to add share link",
|
"Insufficient permissions to add share link",
|
||||||
@@ -2008,10 +2021,11 @@ class DocumentViewSet(
|
|||||||
if not settings.AUDIT_LOG_ENABLED:
|
if not settings.AUDIT_LOG_ENABLED:
|
||||||
return HttpResponseBadRequest("Audit log is disabled")
|
return HttpResponseBadRequest("Audit log is disabled")
|
||||||
try:
|
try:
|
||||||
doc = Document.objects.get(pk=pk)
|
doc = Document.objects.select_related("root_document__owner").get(pk=pk)
|
||||||
|
root_doc = get_root_document(doc)
|
||||||
if not request.user.has_perm("auditlog.view_logentry") or (
|
if not request.user.has_perm("auditlog.view_logentry") or (
|
||||||
doc.owner is not None
|
root_doc.owner is not None
|
||||||
and doc.owner != request.user
|
and root_doc.owner != request.user
|
||||||
and not request.user.is_superuser
|
and not request.user.is_superuser
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden(
|
||||||
@@ -2099,13 +2113,12 @@ class DocumentViewSet(
|
|||||||
message = validated_data.get("message")
|
message = validated_data.get("message")
|
||||||
use_archive_version = validated_data.get("use_archive_version", True)
|
use_archive_version = validated_data.get("use_archive_version", True)
|
||||||
|
|
||||||
documents = Document.objects.filter(pk__in=document_ids)
|
documents = Document.objects.filter(pk__in=document_ids).select_related(
|
||||||
if (
|
"root_document__owner",
|
||||||
request.user is not None
|
)
|
||||||
and documents.exclude(
|
if request.user is not None:
|
||||||
pk__in=permitted_document_ids(request.user),
|
permitted_ids = set(permitted_document_ids(request.user))
|
||||||
).exists()
|
if any(get_root_document(doc).pk not in permitted_ids for doc in documents):
|
||||||
):
|
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -2430,11 +2443,17 @@ class ChatStreamingView(GenericAPIView[Any]):
|
|||||||
|
|
||||||
if doc_id:
|
if doc_id:
|
||||||
try:
|
try:
|
||||||
document = Document.objects.get(id=doc_id)
|
document = Document.objects.select_related(
|
||||||
|
"root_document__owner",
|
||||||
|
).get(id=doc_id)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
return HttpResponseBadRequest("Document not found")
|
return HttpResponseBadRequest("Document not found")
|
||||||
|
|
||||||
if not has_perms_owner_aware(request.user, "view_document", document):
|
if not has_perms_owner_aware(
|
||||||
|
request.user,
|
||||||
|
"view_document",
|
||||||
|
get_root_document(document),
|
||||||
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
documents = Document.objects.filter(pk=document.pk)
|
documents = Document.objects.filter(pk=document.pk)
|
||||||
@@ -4777,6 +4796,7 @@ class ShareLinkBundleViewSet(PassUserMixin, ModelViewSet[ShareLinkBundle]):
|
|||||||
document_ids = serializer.validated_data["document_ids"]
|
document_ids = serializer.validated_data["document_ids"]
|
||||||
documents_qs = Document.objects.filter(pk__in=document_ids).select_related(
|
documents_qs = Document.objects.filter(pk__in=document_ids).select_related(
|
||||||
"owner",
|
"owner",
|
||||||
|
"root_document__owner",
|
||||||
)
|
)
|
||||||
found_ids = set(documents_qs.values_list("pk", flat=True))
|
found_ids = set(documents_qs.values_list("pk", flat=True))
|
||||||
missing = sorted(set(document_ids) - found_ids)
|
missing = sorted(set(document_ids) - found_ids)
|
||||||
@@ -4793,7 +4813,7 @@ class ShareLinkBundleViewSet(PassUserMixin, ModelViewSet[ShareLinkBundle]):
|
|||||||
documents = list(documents_qs)
|
documents = list(documents_qs)
|
||||||
permitted_ids = set(permitted_document_ids(request.user))
|
permitted_ids = set(permitted_document_ids(request.user))
|
||||||
for document in documents:
|
for document in documents:
|
||||||
if document.pk not in permitted_ids:
|
if get_root_document(document).pk not in permitted_ids:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
"document_ids": _(
|
"document_ids": _(
|
||||||
@@ -5587,6 +5607,23 @@ class TrashView(ListModelMixin, PassUserMixin):
|
|||||||
class _TrashPermittedObjectsFilter(PermittedObjectsFilter):
|
class _TrashPermittedObjectsFilter(PermittedObjectsFilter):
|
||||||
include_granted = False
|
include_granted = False
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
if request.user.is_superuser or not request.user.is_active:
|
||||||
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
# A version belongs to whoever owns its root
|
||||||
|
def owned_or_unowned(prefix: str) -> Q:
|
||||||
|
return Q(**{f"{prefix}owner": request.user}) | Q(
|
||||||
|
**{f"{prefix}owner__isnull": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryset.filter(
|
||||||
|
(Q(root_document__isnull=True) & owned_or_unowned(""))
|
||||||
|
| (
|
||||||
|
Q(root_document__isnull=False) & owned_or_unowned("root_document__")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
filter_backends = (_TrashPermittedObjectsFilter,)
|
filter_backends = (_TrashPermittedObjectsFilter,)
|
||||||
pagination_class = StandardPagination
|
pagination_class = StandardPagination
|
||||||
|
|
||||||
@@ -5616,13 +5653,18 @@ class TrashView(ListModelMixin, PassUserMixin):
|
|||||||
if doc_ids is not None
|
if doc_ids is not None
|
||||||
else self.filter_queryset(self.get_queryset()).all()
|
else self.filter_queryset(self.get_queryset()).all()
|
||||||
)
|
)
|
||||||
if docs.exclude(
|
# Versions are authorized by their root document
|
||||||
pk__in=permitted_document_ids(
|
if (
|
||||||
|
docs.annotate(root_id=Coalesce("root_document_id", "id"))
|
||||||
|
.exclude(
|
||||||
|
root_id__in=permitted_document_ids(
|
||||||
request.user,
|
request.user,
|
||||||
perm="delete_document",
|
perm="delete_document",
|
||||||
include_deleted=True,
|
include_deleted=True,
|
||||||
),
|
),
|
||||||
).exists():
|
)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
action = serializer.validated_data.get("action")
|
action = serializer.validated_data.get("action")
|
||||||
if action == "restore":
|
if action == "restore":
|
||||||
|
|||||||
Reference in new issue
Block a user