From dcd055e098120a39d8c7f2bc7eb1f65180bd17d8 Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:40:43 -0700 Subject: [PATCH] Chore: Add shared helpers for granting test permissions and use them --- src/documents/tests/search/test_backend.py | 8 +- .../search/test_permission_field_isolation.py | 6 +- src/documents/tests/test_api_bulk_download.py | 6 +- src/documents/tests/test_api_bulk_edit.py | 43 +++----- src/documents/tests/test_api_chat.py | 8 +- src/documents/tests/test_api_custom_fields.py | 13 +-- .../tests/test_api_document_versions.py | 20 +--- src/documents/tests/test_api_documents.py | 92 +++++++--------- src/documents/tests/test_api_email.py | 6 +- src/documents/tests/test_api_objects.py | 52 +++------ src/documents/tests/test_api_permissions.py | 79 ++++++------- src/documents/tests/test_api_search.py | 40 +++---- src/documents/tests/test_api_status.py | 6 +- src/documents/tests/test_api_tasks.py | 34 ++---- src/documents/tests/test_api_trash.py | 4 +- src/documents/tests/test_bulk_edit.py | 10 +- .../tests/test_management_exporter.py | 6 +- .../tests/test_merge_documents_as_versions.py | 13 +-- .../test_permission_filtering_security.py | 104 +++++++----------- .../tests/test_permitted_objects_filter.py | 8 +- .../tests/test_share_link_bundles.py | 14 +-- src/documents/tests/test_tag_hierarchy.py | 6 +- src/documents/tests/test_views.py | 47 +++----- src/documents/tests/test_workflows.py | 26 ++--- src/paperless_ai/tests/test_ai_classifier.py | 6 +- src/paperless_mail/tests/test_api.py | 33 +++--- src/paperless_mail/tests/test_mail.py | 7 +- src/paperless_mail/tests/test_mail_oauth.py | 9 +- src/paperless_testing/permissions.py | 43 ++++++++ 29 files changed, 319 insertions(+), 430 deletions(-) create mode 100644 src/paperless_testing/permissions.py diff --git a/src/documents/tests/search/test_backend.py b/src/documents/tests/search/test_backend.py index d49e32baa..af12821f8 100644 --- a/src/documents/tests/search/test_backend.py +++ b/src/documents/tests/search/test_backend.py @@ -5,7 +5,6 @@ import pytest from django.contrib.auth.models import Group from django.db import connection from django.test.utils import CaptureQueriesContext -from guardian.shortcuts import assign_perm from pytest_mock import MockerFixture from documents.models import CustomField @@ -23,6 +22,7 @@ from paperless_testing.factories import DocumentFactory from paperless_testing.factories import DocumentTypeFactory from paperless_testing.factories import TagFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_object pytestmark = [pytest.mark.search, pytest.mark.django_db] @@ -188,7 +188,7 @@ class TestAddOrUpdateIds: pk=1, owner=owner, ) - assign_perm("view_document", user, doc) + grant_object(user, doc, "view_document") with backend.batch_update() as batch: batch.add_or_update_ids([doc.pk]) @@ -208,7 +208,7 @@ class TestAddOrUpdateIds: pk=1, owner=owner, ) - assign_perm("view_document", group, doc) + grant_object(group, doc, "view_document") with backend.batch_update() as batch: batch.add_or_update_ids([doc.pk]) @@ -842,7 +842,7 @@ class TestRebuild: content="group secret keyword", owner=owner, ) - assign_perm("view_document", group, doc) + grant_object(group, doc, "view_document") backend.rebuild(Document.objects.all()) diff --git a/src/documents/tests/search/test_permission_field_isolation.py b/src/documents/tests/search/test_permission_field_isolation.py index 784abfaaa..be7c58d0c 100644 --- a/src/documents/tests/search/test_permission_field_isolation.py +++ b/src/documents/tests/search/test_permission_field_isolation.py @@ -18,7 +18,6 @@ from typing import TYPE_CHECKING import pytest from django.contrib.auth.models import Group from django.contrib.auth.models import User -from guardian.shortcuts import assign_perm from documents.models import Correspondent from documents.models import Document @@ -26,6 +25,7 @@ from documents.models import DocumentType from documents.models import StoragePath from documents.models import Tag from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_object if TYPE_CHECKING: from documents.search._backend import TantivyBackend @@ -128,7 +128,7 @@ class TestPermissionFilteringOnIndexedDocuments: checksum="perm-shared-user", owner=owner, ) - assign_perm("view_document", viewer, doc) + grant_object(viewer, doc, "view_document") backend.add_or_update(doc) assert backend.search_ids("invoice", user=viewer) == [doc.pk] @@ -158,7 +158,7 @@ class TestPermissionFilteringOnIndexedDocuments: checksum="perm-shared-group", owner=owner, ) - assign_perm("view_document", group_member.groups.first(), doc) + grant_object(group_member.groups.first(), doc, "view_document") backend.add_or_update(doc) assert backend.search_ids("invoice", user=group_member) == [doc.pk] diff --git a/src/documents/tests/test_api_bulk_download.py b/src/documents/tests/test_api_bulk_download.py index 4eeabb887..1c5558bd6 100644 --- a/src/documents/tests/test_api_bulk_download.py +++ b/src/documents/tests/test_api_bulk_download.py @@ -4,7 +4,6 @@ import json import shutil import zipfile -from django.contrib.auth.models import Permission from django.test import override_settings from django.utils import timezone from rest_framework import status @@ -17,6 +16,7 @@ from documents.tests.utils import SampleDirMixin from documents.tests.utils import read_streaming_response from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase): @@ -327,9 +327,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase): def test_download_insufficient_permissions(self) -> None: user = UserFactory(username="temp_user") - user.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(user, "view_document") self.client.force_authenticate(user=user) self.doc2.owner = self.user diff --git a/src/documents/tests/test_api_bulk_edit.py b/src/documents/tests/test_api_bulk_edit.py index d5a0e92c3..275e3aa27 100644 --- a/src/documents/tests/test_api_bulk_edit.py +++ b/src/documents/tests/test_api_bulk_edit.py @@ -2,10 +2,8 @@ import json from unittest import mock from auditlog.models import LogEntry -from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.test import override_settings -from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase @@ -17,6 +15,9 @@ from documents.models import StoragePath from documents.models import Tag from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_all_global +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestBulkEditAPI(DirectoriesMixin, APITestCase): @@ -286,7 +287,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): ) -> None: self.setup_mock(m, "modify_custom_fields") user = UserFactory(username="doc-owner") - user.user_permissions.add(Permission.objects.get(codename="change_document")) + grant_global(user, "change_document") other_user = UserFactory(username="other-user") source_doc = Document.objects.create( checksum="source", @@ -789,9 +790,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): def test_api_bulk_edit_with_all_true_resolves_owned_duplicates(self, m) -> None: self.setup_mock(m, "set_storage_path") user = UserFactory(username="duplicate-owner") - user.user_permissions.add( - Permission.objects.get(codename="change_document"), - ) + grant_global(user, "change_document") first_duplicate = Document.objects.create( checksum="owned-duplicate", title="First duplicate", @@ -1179,7 +1178,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): user1 = User.objects.create(username="user1") self.client.force_authenticate(user=user1) - assign_perm("view_document", user1, self.doc2) + grant_object(user1, self.doc2, "view_document") response = self.client.post( "/api/documents/selection_data/", @@ -1189,9 +1188,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - user1.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(user1, "view_document") user1 = User.objects.get(pk=user1.pk) self.client.force_authenticate(user=user1) response = self.client.post( @@ -1534,7 +1531,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.save() user1 = User.objects.create(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() self.client.force_authenticate(user=user1) @@ -1588,8 +1585,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.save() user1 = User.objects.create(username="user1") - assign_perm("view_document", user1, self.doc1) - user1.user_permissions.add(*Permission.objects.all()) + grant_object(user1, self.doc1, "view_document") + grant_all_global(user1) user1.save() self.client.force_authenticate(user=user1) @@ -1610,7 +1607,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): m.assert_not_called() self.assertEqual(response.content, b"Insufficient permissions") - assign_perm("change_document", user1, self.doc1) + grant_object(user1, self.doc1, "change_document") response = self.client.post( "/api/documents/bulk_edit/", @@ -1820,7 +1817,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.save() user1 = User.objects.create(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() self.client.force_authenticate(user=user1) @@ -1881,7 +1878,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.save() user1 = User.objects.create(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() self.client.force_authenticate(user=user1) @@ -1921,10 +1918,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): def test_merge_and_delete_requires_change_permission(self, m) -> None: self.setup_mock(m, "merge") user = UserFactory(username="no-change") - user.user_permissions.add( - Permission.objects.get(codename="add_document"), - Permission.objects.get(codename="delete_document"), - ) + grant_global(user, "add_document", "delete_document") self.client.force_authenticate(user=user) response = self.client.post( @@ -2311,7 +2305,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.save() user1 = User.objects.create(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() self.client.force_authenticate(user=user1) @@ -2374,10 +2368,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.setup_mock(edit_pdf_mock, "edit_pdf") self.setup_mock(remove_password_mock, "remove_password") user = UserFactory(username="no-delete") - user.user_permissions.add( - Permission.objects.get(codename="add_document"), - Permission.objects.get(codename="change_document"), - ) + grant_global(user, "add_document", "change_document") self.client.force_authenticate(user=user) cases = [ @@ -2464,7 +2455,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.save() user1 = User.objects.create(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() self.client.force_authenticate(user=user1) diff --git a/src/documents/tests/test_api_chat.py b/src/documents/tests/test_api_chat.py index ccc0bb28f..f19c4aa43 100644 --- a/src/documents/tests/test_api_chat.py +++ b/src/documents/tests/test_api_chat.py @@ -4,15 +4,15 @@ from typing import TYPE_CHECKING from unittest import mock import pytest -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APIClient from rest_framework.test import APITestCase from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global if TYPE_CHECKING: + from django.contrib.auth.models import User from pytest_mock import MockerFixture @@ -115,9 +115,7 @@ class TestChatStreamingViewUnrestrictedFlag: needs to reach the view at all. Model-level only: says nothing about which documents (if any) this user can actually see. """ - regular_user.user_permissions.add( - *Permission.objects.filter(codename="view_document"), - ) + grant_global(regular_user, "view_document") return user_client @pytest.mark.parametrize( diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py index 8d604e60a..a1823c501 100644 --- a/src/documents/tests/test_api_custom_fields.py +++ b/src/documents/tests/test_api_custom_fields.py @@ -5,7 +5,6 @@ from unittest.mock import ANY from django.contrib.auth.models import Permission from django.test import override_settings -from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase @@ -14,6 +13,8 @@ from documents.models import CustomFieldInstance from documents.models import Document from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): @@ -1175,9 +1176,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): self, ) -> None: source_owner = UserFactory(username="source-owner") - source_owner.user_permissions.add( - Permission.objects.get(codename="change_document"), - ) + grant_global(source_owner, "change_document") other_user = UserFactory(username="other-user") source_doc = Document.objects.create( @@ -1222,9 +1221,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): self, ) -> None: source_owner = UserFactory(username="source-owner") - source_owner.user_permissions.add( - Permission.objects.get(codename="change_document"), - ) + grant_global(source_owner, "change_document") other_user = UserFactory(username="other-user") source_doc = Document.objects.create( @@ -1244,7 +1241,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): data_type=CustomField.FieldDataType.DOCUMENTLINK, ) - assign_perm("change_document", source_owner, target_doc) + grant_object(source_owner, target_doc, "change_document") self.client.force_authenticate(user=source_owner) resp = self.client.patch( diff --git a/src/documents/tests/test_api_document_versions.py b/src/documents/tests/test_api_document_versions.py index edbabeda5..5bc02f9c6 100644 --- a/src/documents/tests/test_api_document_versions.py +++ b/src/documents/tests/test_api_document_versions.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from unittest import mock from auditlog.models import LogEntry # type: ignore[import-untyped] -from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase as DjangoTestCase @@ -22,6 +21,7 @@ from documents.versioning import annotate_effective_content from documents.views import DocumentSelectionMixin from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global if TYPE_CHECKING: from pathlib import Path @@ -91,9 +91,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): def test_root_endpoint_returns_403_when_user_lacks_permission(self) -> None: owner = UserFactory(username="owner") viewer = UserFactory(username="viewer") - viewer.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(viewer, "view_document") root = Document.objects.create( title="root", checksum="root", @@ -285,9 +283,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): def test_delete_version_returns_403_without_permission(self) -> None: owner = UserFactory(username="owner") other = UserFactory(username="other") - other.user_permissions.add( - Permission.objects.get(codename="delete_document"), - ) + grant_global(other, "delete_document") root = Document.objects.create( title="root", checksum="root", @@ -373,9 +369,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): def test_update_version_label_returns_403_without_permission(self) -> None: owner = UserFactory(username="owner") other = UserFactory(username="other") - other.user_permissions.add( - Permission.objects.get(codename="change_document"), - ) + grant_global(other, "change_document") root = Document.objects.create( title="root", checksum="root", @@ -555,9 +549,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): def test_metadata_returns_403_when_user_lacks_permission(self) -> None: owner = UserFactory(username="owner") other = UserFactory(username="other") - other.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(other, "view_document") doc = Document.objects.create( title="root", checksum="root", @@ -673,7 +665,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): def test_update_version_requires_global_change_permission(self) -> None: user = UserFactory(username="add-only") - user.user_permissions.add(Permission.objects.get(codename="add_document")) + grant_global(user, "add_document") root = Document.objects.create( title="root", checksum="root", diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index e6dbf4cf9..b5640c646 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -23,7 +23,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.db import DataError from django.test import override_settings from django.utils import timezone -from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase @@ -54,6 +53,9 @@ from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import DocumentFactory from paperless_testing.factories import TagFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_all_global +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): @@ -360,8 +362,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): user1 = UserFactory(username="test1") user2 = UserFactory(username="test2") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - user2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user1, "view_document") + grant_global(user2, "view_document") self.client.force_authenticate(user2) @@ -384,7 +386,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): response = self.client.get(f"/api/documents/{doc.pk}/thumb/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - assign_perm("view_document", user2, doc) + grant_object(user2, doc, "view_document") response = self.client.get(f"/api/documents/{doc.pk}/download/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -762,7 +764,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): """ # No auditlog permissions user = UserFactory(username="test") - user.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user, "view_document") self.client.force_authenticate(user=user) doc = Document.objects.create( title="First title", @@ -1076,9 +1078,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): def test_has_duplicates_filter_respects_document_permissions(self) -> None: owner = UserFactory(username="duplicate-owner") requester = UserFactory(username="duplicate-requester") - requester.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(requester, "view_document") visible_document = Document.objects.create( title="visible document", checksum="permission-match", @@ -1097,7 +1097,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): [document["id"] for document in response.data["results"]], ) - assign_perm("view_document", requester, hidden_duplicate) + grant_object(requester, hidden_duplicate, "view_document") response = self.client.get("/api/documents/?has_duplicates=true") self.assertIn( visible_document.id, @@ -1320,8 +1320,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): """ u1 = UserFactory(username="user1") u2 = UserFactory(username="user2") - u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(u1, "view_document") + grant_global(u2, "view_document") u1_doc1 = Document.objects.create( title="none1", @@ -1354,7 +1354,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): ) self.client.force_authenticate(user=u1) - assign_perm("view_document", u1, u2_doc2) + grant_object(u1, u2_doc2, "view_document") # Will not show any u1 docs or u2_doc1 which isn't shared response = self.client.get(f"/api/documents/?owner__id__none={u1.id}") @@ -1401,7 +1401,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): [u1_doc1.id, u1_doc2.id, u2_doc2.id], ) - assign_perm("view_document", u2, u1_doc1) + grant_object(u2, u1_doc1, "view_document") # Will show only documents shared by user response = self.client.get(f"/api/documents/?shared_by__id={u1.id}") @@ -1426,7 +1426,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): (regression test for https://github.com/paperless-ngx/paperless-ngx/issues/13331) """ user = UserFactory(username="user1") - user.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user, "view_document") group = Group.objects.create(name="group1") user.groups.add(group) @@ -1434,7 +1434,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): tag2 = TagFactory() doc = DocumentFactory(title="shared", owner=user) doc.tags.add(tag1, tag2) - assign_perm("view_document", group, doc) + grant_object(group, doc, "view_document") self.client.force_authenticate(user=user) response = self.client.get( @@ -1455,9 +1455,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): """ owner = UserFactory(username="owner1") stranger = UserFactory(username="stranger1") - stranger.user_permissions.add( - *Permission.objects.filter(codename="view_document"), - ) + grant_global(stranger, "view_document") DocumentFactory(title="private", owner=owner) @@ -1479,13 +1477,13 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): member = UserFactory(username="member1") non_member = UserFactory(username="nonmember1") for u in (member, non_member): - u.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(u, "view_document") group = Group.objects.create(name="group2") member.groups.add(group) doc = DocumentFactory(title="shared2", owner=owner) - assign_perm("view_document", group, doc) + grant_object(group, doc, "view_document") self.client.force_authenticate(user=member) response = self.client.get("/api/documents/") @@ -1819,9 +1817,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): def test_statistics_with_statistics_permission(self) -> None: owner = UserFactory(username="owner") stats_user = UserFactory(username="stats-user") - stats_user.user_permissions.add( - Permission.objects.get(codename="view_global_statistics"), - ) + grant_global(stats_user, "view_global_statistics") inbox_tag = Tag.objects.create( name="stats_inbox", @@ -2808,9 +2804,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): sort_field="", ) - assign_perm("view_savedview", u1, v2) - assign_perm("change_savedview", u1, v2) - assign_perm("view_savedview", u1, v3) + grant_object(u1, v2, "view_savedview") + grant_object(u1, v2, "change_savedview") + grant_object(u1, v3, "view_savedview") self.client.force_authenticate(user=u1) @@ -3570,7 +3566,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - Notes are neither created nor deleted """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() user2 = UserFactory(username="test2") @@ -3593,7 +3589,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): self.assertEqual(resp.content, b"Insufficient permissions to view notes") self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - assign_perm("view_document", user1, doc) + grant_object(user1, doc, "view_document") resp = self.client.post( f"/api/documents/{doc.pk}/notes/", @@ -3618,11 +3614,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): def test_notes_require_global_document_permissions(self) -> None: user = UserFactory(username="note_editor") - user.user_permissions.add( - *Permission.objects.filter( - codename__in=["view_note", "add_note", "delete_note"], - ), - ) + grant_global(user, "view_note", "add_note", "delete_note") doc = Document.objects.create( title="test", mime_type="application/pdf", @@ -3635,9 +3627,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): response = self.client.get(f"/api/documents/{doc.pk}/notes/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - user.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(user, "view_document") user = User.objects.get(pk=user.pk) self.client.force_authenticate(user) response = self.client.get(f"/api/documents/{doc.pk}/notes/") @@ -3649,9 +3639,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - user.user_permissions.add( - Permission.objects.get(codename="change_document"), - ) + grant_global(user, "change_document") user = User.objects.get(pk=user.pk) self.client.force_authenticate(user) response = self.client.post( @@ -3799,11 +3787,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - Non-unique items are not allowed """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="add_tag")) + grant_global(user1, "add_tag") user1.save() user2 = UserFactory(username="test2") - user2.user_permissions.add(*Permission.objects.filter(codename="add_tag")) + grant_global(user2, "add_tag") user2.save() # User 1 creates tag 1 owned by user 1 by default @@ -3859,11 +3847,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - Non-unique items are not allowed on update """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="change_tag")) + grant_global(user1, "change_tag") user1.save() user2 = UserFactory(username="test2") - user2.user_permissions.add(*Permission.objects.filter(codename="change_tag")) + grant_global(user2, "change_tag") user2.save() # Create name tag 1 owned by user 1 @@ -3995,7 +3983,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - Links only shown if user has permissions """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() user2 = UserFactory(username="test2") @@ -4018,7 +4006,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): self.assertEqual(resp.content, b"Insufficient permissions to add share link") self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - assign_perm("change_document", user1, doc) + grant_object(user1, doc, "change_document") resp = self.client.get( f"/api/documents/{doc.pk}/share_links/", @@ -4036,7 +4024,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - Share link creation is denied until view permission is granted """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="add_sharelink")) + grant_global(user1, "add_sharelink") user1.save() user2 = UserFactory(username="test2") @@ -4061,7 +4049,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): ) self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN) - assign_perm("view_document", user1, doc) + grant_object(user1, doc, "view_document") create_resp = self.client.post( "/api/share_links/", @@ -4073,9 +4061,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): ) self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN) - user1.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(user1, "view_document") user1 = User.objects.get(pk=user1.pk) self.client.force_authenticate(user1) create_resp = self.client.post( @@ -4099,7 +4085,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - ASN +1 from user2's doc is returned for user1 """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() user2 = UserFactory(username="test2") @@ -4143,7 +4129,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - ASN 1 is returned """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() doc1 = Document.objects.create( @@ -4350,7 +4336,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): - Error response is returned """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user1.save() doc = Document.objects.create( diff --git a/src/documents/tests/test_api_email.py b/src/documents/tests/test_api_email.py index a34015ece..d26f26d85 100644 --- a/src/documents/tests/test_api_email.py +++ b/src/documents/tests/test_api_email.py @@ -2,7 +2,6 @@ import json import shutil from unittest import mock -from django.contrib.auth.models import Permission from django.core import mail from django.test import override_settings from rest_framework import status @@ -12,6 +11,7 @@ from documents.models import Document from documents.tests.utils import SampleDirMixin from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase): @@ -304,7 +304,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase): - Forbidden response is returned """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user1, "view_document") doc_owned = Document.objects.create( title="owned_doc", @@ -339,7 +339,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase): - Request succeeds """ user1 = UserFactory(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user1, "view_document") self.client.force_authenticate(user1) diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index e1e5b1fad..a8047d0c6 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -3,12 +3,10 @@ import json from unittest import mock from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.db import connection from django.test import override_settings from django.test.utils import CaptureQueriesContext -from guardian.shortcuts import assign_perm from guardian.shortcuts import get_groups_with_perms from guardian.shortcuts import get_users_with_perms from rest_framework import status @@ -23,6 +21,8 @@ from documents.models import StoragePath from documents.models import Tag from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestApiObjects(DirectoriesMixin, APITestCase): @@ -164,9 +164,7 @@ class TestApiObjects(DirectoriesMixin, APITestCase): ) user = UserFactory(username="regular") - user.user_permissions.add( - Permission.objects.get(codename="view_correspondent"), - ) + grant_global(user, "view_correspondent") self.client.force_authenticate(user=user) response = self.client.get("/api/correspondents/?last_correspondence=true") @@ -458,9 +456,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): def test_test_storage_path_requires_document_view_permission(self) -> None: owner = UserFactory(username="owner") unprivileged = UserFactory(username="unprivileged") - unprivileged.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(unprivileged, "view_document") document = Document.objects.create( mime_type="application/pdf", owner=owner, @@ -490,7 +486,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): title="Shared", checksum="123", ) - assign_perm("view_document", viewer, document) + grant_object(viewer, document, "view_document") self.client.force_authenticate(user=viewer) response = self.client.post( @@ -505,9 +501,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - viewer.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(viewer, "view_document") viewer = User.objects.get(pk=viewer.pk) self.client.force_authenticate(user=viewer) response = self.client.post( @@ -551,9 +545,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): password="password", email="owner@example.com", ) - owner.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(owner, "view_document") document = Document.objects.create( mime_type="application/pdf", owner=owner, @@ -628,10 +620,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): title="Document", checksum="123", ) - assign_perm("view_document", viewer, document) - viewer.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_object(viewer, document, "view_document") + grant_global(viewer, "view_document") self.client.force_authenticate(user=viewer) response = self.client.post( @@ -718,10 +708,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): checksum="123", ) document.tags.add(private_tag) - assign_perm("view_document", viewer, document) - viewer.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_object(viewer, document, "view_document") + grant_global(viewer, "view_document") self.client.force_authenticate(user=viewer) response = self.client.post( @@ -774,10 +762,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): field=custom_field, value_int=42, ) - assign_perm("view_document", viewer, document) - viewer.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_object(viewer, document, "view_document") + grant_global(viewer, "view_document") self.client.force_authenticate(user=viewer) response = self.client.post( @@ -1031,9 +1017,7 @@ class TestBulkEditObjects(APITestCase): THEN: - User is able to delete objects """ - self.user1.user_permissions.add( - *Permission.objects.filter(codename="delete_tag"), - ) + grant_global(self.user1, "delete_tag") self.user1.save() self.client.force_authenticate(user=self.user1) @@ -1063,9 +1047,7 @@ class TestBulkEditObjects(APITestCase): self.t2.owner = User.objects.get(username="temp_admin") self.t2.save() - self.user1.user_permissions.add( - *Permission.objects.filter(codename="delete_tag"), - ) + grant_global(self.user1, "delete_tag") self.user1.save() self.client.force_authenticate(user=self.user1) @@ -1098,9 +1080,7 @@ class TestBulkEditObjects(APITestCase): self.t2.owner = User.objects.get(username="temp_admin") self.t2.save() - self.user1.user_permissions.add( - *Permission.objects.filter(codename="delete_tag"), - ) + grant_global(self.user1, "delete_tag") self.user1.save() self.client.force_authenticate(user=self.user1) diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py index e91eca1e7..7f9c3dde2 100644 --- a/src/documents/tests/test_api_permissions.py +++ b/src/documents/tests/test_api_permissions.py @@ -5,9 +5,7 @@ from unittest import mock from allauth.mfa.models import Authenticator from allauth.mfa.totp.internal import auth as totp_auth from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission from django.contrib.auth.models import User -from guardian.shortcuts import assign_perm from guardian.shortcuts import get_perms from guardian.shortcuts import get_users_with_perms from rest_framework import status @@ -21,6 +19,9 @@ from documents.models import StoragePath from documents.models import Tag from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_all_global +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestApiAuth(DirectoriesMixin, APITestCase): @@ -139,7 +140,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase): def test_api_sufficient_permissions(self) -> None: user = UserFactory(username="test") - user.user_permissions.add(*Permission.objects.all()) + grant_all_global(user) user.is_staff = True self.client.force_authenticate(user) @@ -169,7 +170,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase): def test_api_get_object_permissions(self) -> None: user1 = UserFactory(username="test1") user2 = UserFactory(username="test2") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user1, "view_document") self.client.force_authenticate(user1) self.assertEqual( @@ -420,10 +421,10 @@ class TestApiAuth(DirectoriesMixin, APITestCase): doc.owner = user1 doc.save() - assign_perm("view_document", user2, doc) - assign_perm("change_document", user2, doc) - assign_perm("view_document", group1, doc) - assign_perm("change_document", group1, doc) + grant_object(user2, doc, "view_document") + grant_object(user2, doc, "change_document") + grant_object(group1, doc, "view_document") + grant_object(group1, doc, "change_document") self.client.force_authenticate(user1) @@ -449,9 +450,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase): def test_document_permissions_change_requires_owner(self) -> None: owner = UserFactory(username="owner") editor = UserFactory(username="editor") - editor.user_permissions.add( - *Permission.objects.all(), - ) + grant_all_global(editor) doc = Document.objects.create( title="Ownered doc", @@ -461,8 +460,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase): owner=owner, ) - assign_perm("view_document", editor, doc) - assign_perm("change_document", editor, doc) + grant_object(editor, doc, "view_document") + grant_object(editor, doc, "change_document") self.client.force_authenticate(editor) response = self.client.patch( @@ -501,7 +500,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase): def test_dynamic_permissions_fields(self) -> None: user1 = UserFactory(username="user1") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(user1, "view_document") user2 = UserFactory(username="user2") Document.objects.create(title="Test", content="content 1", checksum="1") @@ -524,10 +523,10 @@ class TestApiAuth(DirectoriesMixin, APITestCase): owner=user1, ) - assign_perm("view_document", user1, doc2) - assign_perm("view_document", user1, doc3) - assign_perm("change_document", user1, doc3) - assign_perm("view_document", user2, doc4) + grant_object(user1, doc2, "view_document") + grant_object(user1, doc3, "view_document") + grant_object(user1, doc3, "change_document") + grant_object(user2, doc4, "view_document") self.client.force_authenticate(user1) @@ -860,9 +859,7 @@ class TestApiUser(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) regular_user = UserFactory(username="regular_user") - regular_user.user_permissions.add( - *Permission.objects.all(), - ) + grant_all_global(regular_user) self.client.force_authenticate(regular_user) Authenticator.objects.create( user=user1, @@ -887,7 +884,7 @@ class TestApiUser(DirectoriesMixin, APITestCase): """ user1 = UserFactory(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user2 = UserFactory(username="user2", superuser=True) self.client.force_authenticate(user1) @@ -974,7 +971,7 @@ class TestApiUser(DirectoriesMixin, APITestCase): """ user1 = UserFactory(username="user1") - user1.user_permissions.add(*Permission.objects.all()) + grant_all_global(user1) user2 = UserFactory(username="user2", superuser=True) self.client.force_authenticate(user1) @@ -1277,7 +1274,7 @@ class TestBulkEditObjectPermissions(APITestCase): }, } - assign_perm("view_tag", self.user3, self.t1) + grant_object(self.user3, self.t1, "view_tag") self.t1.owner = self.user3 self.t1.save() @@ -1374,13 +1371,9 @@ class TestBulkEditObjectPermissions(APITestCase): """ self.t1.owner = self.user2 self.t1.save() - assign_perm("view_tag", self.user1, self.t1) - assign_perm("change_tag", self.user1, self.t1) - self.user1.user_permissions.add( - *Permission.objects.filter( - codename__in=["view_tag", "change_tag"], - ), - ) + grant_object(self.user1, self.t1, "view_tag") + grant_object(self.user1, self.t1, "change_tag") + grant_global(self.user1, "view_tag", "change_tag") user1 = User.objects.get(pk=self.user1.pk) self.client.force_authenticate(user=user1) @@ -1427,13 +1420,9 @@ class TestBulkEditObjectPermissions(APITestCase): """ owned = Tag.objects.create(name="owned", owner=self.user1) shared = Tag.objects.create(name="shared", owner=self.user2) - assign_perm("view_tag", self.user1, shared) - assign_perm("change_tag", self.user1, shared) - self.user1.user_permissions.add( - *Permission.objects.filter( - codename__in=["view_tag", "change_tag"], - ), - ) + grant_object(self.user1, shared, "view_tag") + grant_object(self.user1, shared, "change_tag") + grant_global(self.user1, "view_tag", "change_tag") user1 = User.objects.get(pk=self.user1.pk) self.client.force_authenticate(user=user1) @@ -1474,14 +1463,10 @@ class TestBulkEditObjectPermissions(APITestCase): """ self.t1.owner = self.user2 self.t1.save() - assign_perm("view_tag", self.user1, self.t1) - assign_perm("change_tag", self.user1, self.t1) - assign_perm("delete_tag", self.user1, self.t1) - self.user1.user_permissions.add( - *Permission.objects.filter( - codename__in=["view_tag", "change_tag", "delete_tag"], - ), - ) + grant_object(self.user1, self.t1, "view_tag") + grant_object(self.user1, self.t1, "change_tag") + grant_object(self.user1, self.t1, "delete_tag") + grant_global(self.user1, "view_tag", "change_tag", "delete_tag") user1 = User.objects.get(pk=self.user1.pk) self.client.force_authenticate(user=user1) @@ -1586,7 +1571,7 @@ class TestBulkEditObjectPermissions(APITestCase): - Request succeeds and null is treated as an empty user list, so the existing view permission is removed """ - assign_perm("view_tag", self.user1, self.t1) + grant_object(self.user1, self.t1, "view_tag") response = self.client.post( "/api/bulk_edit_objects/", diff --git a/src/documents/tests/test_api_search.py b/src/documents/tests/test_api_search.py index 887068ea3..102736139 100644 --- a/src/documents/tests/test_api_search.py +++ b/src/documents/tests/test_api_search.py @@ -9,7 +9,6 @@ from django.contrib.auth.models import Group from django.contrib.auth.models import Permission from django.test import override_settings from django.utils import timezone -from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase @@ -31,6 +30,8 @@ from paperless_mail.models import MailRule from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import DocumentFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object pytestmark = pytest.mark.search @@ -951,7 +952,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): """ u1 = UserFactory(username="user1") u2 = UserFactory(username="user2") - u1.user_permissions.add(Permission.objects.get(codename="view_document")) + grant_global(u1, "view_document") self.client.force_authenticate(user=u1) @@ -991,7 +992,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, ["applebaum", "apples"]) - assign_perm("view_document", u1, d3) + grant_object(u1, d3, "view_document") backend.add_or_update(d3) response = self.client.get("/api/search/autocomplete/?term=app") @@ -1002,7 +1003,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): user = UserFactory(username="group-user") owner = UserFactory(username="document-owner") group = Group.objects.create(name="temporary-viewers") - user.user_permissions.add(Permission.objects.get(codename="view_document")) + grant_global(user, "view_document") user.groups.add(group) document = Document.objects.create( @@ -1011,7 +1012,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): checksum="group-revocation", owner=owner, ) - assign_perm("view_document", group, document) + grant_object(group, document, "view_document") get_backend().add_or_update(document) self.client.force_authenticate(user=user) @@ -1093,9 +1094,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): def test_search_spelling_suggestion_suppressed_for_private_terms(self) -> None: owner = UserFactory(username="owner") attacker = UserFactory(username="attacker") - attacker.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(attacker, "view_document") backend = get_backend() for i in range(5): @@ -1224,9 +1223,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): """ owner = UserFactory(username="owner") attacker = UserFactory(username="attacker") - attacker.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(attacker, "view_document") private_seed = Document.objects.create( title="private bank statement", @@ -1537,8 +1534,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): superuser = UserFactory(username="superuser", superuser=True) u1 = UserFactory(username="user1") u2 = UserFactory(username="user2") - u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(u1, "view_document") + grant_global(u2, "view_document") Document.objects.create(checksum="1", content="test 1", owner=u1) Document.objects.create(checksum="2", content="test 2", owner=u2) @@ -1590,8 +1587,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): """ u1 = UserFactory(username="user1") u2 = UserFactory(username="user2") - u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + grant_global(u1, "view_document") + grant_global(u2, "view_document") d1 = Document.objects.create(checksum="1", content="test 1", owner=u1) d2 = Document.objects.create(checksum="2", content="test 2", owner=u2) @@ -1616,9 +1613,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): r = self.client.get("/api/documents/?query=test&owner__isnull=true") self.assertEqual(r.data["count"], 1) - assign_perm("view_document", u1, d2) - assign_perm("view_document", u1, d3) - assign_perm("view_document", u2, d1) + grant_object(u1, d2, "view_document") + grant_object(u1, d3, "view_document") + grant_object(u2, d1, "view_document") backend.add_or_update(d1) backend.add_or_update(d2) @@ -1925,7 +1922,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): sort_field="", owner=user2, ) - assign_perm("view_savedview", user1, shared_view) + grant_object(user1, shared_view, "view_savedview") mail_account1 = MailAccount.objects.create(name="bank mail account 1") mail_account2 = MailAccount.objects.create(name="mail account 2") mail_rule1 = MailRule.objects.create( @@ -2020,10 +2017,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): def test_global_search_filters_owned_mail_objects(self) -> None: user1 = UserFactory(username="mail-search-user") user2 = UserFactory(username="other-mail-search-user") - user1.user_permissions.add( - Permission.objects.get(codename="view_mailaccount"), - Permission.objects.get(codename="view_mailrule"), - ) + grant_global(user1, "view_mailaccount", "view_mailrule") own_account = MailAccount.objects.create( name="bank owned account", diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py index effee88f8..6f200e54f 100644 --- a/src/documents/tests/test_api_status.py +++ b/src/documents/tests/test_api_status.py @@ -5,7 +5,6 @@ from datetime import timedelta from pathlib import Path from unittest import mock -from django.contrib.auth.models import Permission from django.test import override_settings from django.utils import timezone from rest_framework import status @@ -16,6 +15,7 @@ from documents.permissions import has_system_status_permission from paperless import version from paperless_testing.factories import PaperlessTaskFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global class TestSystemStatus(APITestCase): @@ -106,9 +106,7 @@ class TestSystemStatus(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) user = UserFactory(username="status_user") - user.user_permissions.add( - Permission.objects.get(codename="view_system_monitoring"), - ) + grant_global(user, "view_system_monitoring") self.client.force_login(user) response = self.client.get(self.ENDPOINT) diff --git a/src/documents/tests/test_api_tasks.py b/src/documents/tests/test_api_tasks.py index 71805961a..31ec632ec 100644 --- a/src/documents/tests/test_api_tasks.py +++ b/src/documents/tests/test_api_tasks.py @@ -11,10 +11,8 @@ from datetime import timedelta from unittest import mock import pytest -from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.utils import timezone -from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APIClient @@ -22,6 +20,8 @@ from documents.filters import PaperlessTaskFilterSet from documents.models import PaperlessTask from paperless_testing.factories import DocumentFactory from paperless_testing.factories import PaperlessTaskFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object pytestmark = pytest.mark.api @@ -348,9 +348,7 @@ class TestGetTasksV10: user_client: APIClient, ) -> None: """Regular users see their own tasks and unowned (system) tasks; other users' tasks are hidden.""" - regular_user.user_permissions.add( - Permission.objects.get(codename="view_paperlesstask"), - ) + grant_global(regular_user, "view_paperlesstask") PaperlessTaskFactory(owner=admin_user) # other user — not visible unowned_task = PaperlessTaskFactory() # unowned (system task) — visible @@ -586,9 +584,7 @@ class TestGetTasksV9: regular_user: User, ) -> None: """Non-staff users see their own tasks plus unowned tasks via v9 API.""" - regular_user.user_permissions.add( - Permission.objects.get(codename="view_paperlesstask"), - ) + grant_global(regular_user, "view_paperlesstask") client = APIClient() client.force_authenticate(user=regular_user) @@ -734,9 +730,7 @@ class TestAcknowledge: user_client: APIClient, ) -> None: """Users granted change_paperlesstask permission can acknowledge tasks.""" - regular_user.user_permissions.add( - Permission.objects.get(codename="change_paperlesstask"), - ) + grant_global(regular_user, "change_paperlesstask") regular_user.save() task = PaperlessTaskFactory() @@ -803,9 +797,7 @@ class TestSummaryPermissions: regular_user, ) -> None: """A user with view_system_monitoring but no document permissions can access summary/.""" - regular_user.user_permissions.add( - Permission.objects.get(codename="view_system_monitoring"), - ) + grant_global(regular_user, "view_system_monitoring") response = user_client.get(ENDPOINT + "summary/") @@ -818,9 +810,7 @@ class TestSummaryPermissions: admin_user, ) -> None: """Monitoring user sees aggregate data for all tasks, not just unowned ones.""" - regular_user.user_permissions.add( - Permission.objects.get(codename="view_system_monitoring"), - ) + grant_global(regular_user, "view_system_monitoring") PaperlessTaskFactory( owner=admin_user, task_type=PaperlessTask.TaskType.CONSUME_FILE, @@ -841,9 +831,7 @@ class TestSummaryPermissions: ) -> None: """A regular user with view_paperlesstask but not view_system_monitoring sees only their own tasks and unowned tasks in the summary, not other users' tasks.""" - regular_user.user_permissions.add( - Permission.objects.get(codename="view_paperlesstask"), - ) + grant_global(regular_user, "view_paperlesstask") PaperlessTaskFactory( owner=regular_user, @@ -1008,9 +996,7 @@ class TestDuplicateDocumentsPermissions: @pytest.fixture() def user_v9_client(self, regular_user: User) -> APIClient: - regular_user.user_permissions.add( - Permission.objects.get(codename="view_paperlesstask"), - ) + grant_global(regular_user, "view_paperlesstask") client = APIClient() client.force_authenticate(user=regular_user) client.credentials(HTTP_ACCEPT=ACCEPT_V9) @@ -1081,7 +1067,7 @@ class TestDuplicateDocumentsPermissions: ) -> None: """A user with explicit guardian view_document permission sees the duplicate_of document.""" doc = DocumentFactory(owner=admin_user, title="Granted Doc") - assign_perm("view_document", regular_user, doc) + grant_object(regular_user, doc, "view_document") PaperlessTaskFactory( owner=regular_user, status=PaperlessTask.Status.SUCCESS, diff --git a/src/documents/tests/test_api_trash.py b/src/documents/tests/test_api_trash.py index ac0e00ead..792f9ca00 100644 --- a/src/documents/tests/test_api_trash.py +++ b/src/documents/tests/test_api_trash.py @@ -1,6 +1,5 @@ from datetime import date -from django.contrib.auth.models import Permission from django.core.cache import cache from rest_framework import status from rest_framework.test import APITestCase @@ -8,6 +7,7 @@ from rest_framework.test import APITestCase from documents.models import Document from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_all_global class TestTrashAPI(DirectoriesMixin, APITestCase): @@ -15,7 +15,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase): super().setUp() self.user = UserFactory(username="temp_admin") - self.user.user_permissions.add(*Permission.objects.all()) + grant_all_global(self.user) self.client.force_authenticate(user=self.user) cache.clear() diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index 1970511cc..1b67a320f 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -10,7 +10,6 @@ from django.contrib.auth.models import User from django.db import connection from django.test import TestCase from django.test.utils import CaptureQueriesContext -from guardian.shortcuts import assign_perm from guardian.shortcuts import get_groups_with_perms from guardian.shortcuts import get_users_with_perms @@ -24,6 +23,7 @@ from documents.models import StoragePath from documents.models import Tag from documents.permissions import set_permissions_for_objects from paperless_testing.dirs import DirectoriesMixin +from paperless_testing.permissions import grant_object class TestBulkEdit(DirectoriesMixin, TestCase): @@ -440,7 +440,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): def test_set_permissions(self, m) -> None: doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id] - assign_perm("view_document", self.group1, self.doc1) + grant_object(self.group1, self.doc1, "view_document") permissions = { "view": { @@ -482,8 +482,8 @@ class TestBulkEdit(DirectoriesMixin, TestCase): self.doc1.owner = self.user1 self.doc1.save() - assign_perm("view_document", self.user1, self.doc1) - assign_perm("view_document", self.group1, self.doc1) + grant_object(self.user1, self.doc1, "view_document") + grant_object(self.group1, self.doc1, "view_document") permissions = { "view": { @@ -609,7 +609,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): self.doc1.owner = self.user1 self.doc1.save() self.user1.groups.add(self.group1) - assign_perm("view_document", self.group1, self.doc1) + grant_object(self.group1, self.doc1, "view_document") bulk_edit.set_permissions( [self.doc1.id], diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index 95dbfc429..4083244bb 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -26,7 +26,6 @@ from django.test import override_settings from django.utils import timezone from guardian.models import GroupObjectPermission from guardian.models import UserObjectPermission -from guardian.shortcuts import assign_perm from documents.management.commands import document_exporter from documents.models import Correspondent @@ -51,6 +50,7 @@ from documents.tests.utils import SampleDirMixin from paperless_mail.models import MailAccount from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import paperless_environment +from paperless_testing.permissions import grant_object @pytest.mark.management @@ -105,8 +105,8 @@ class TestExportImport( user=self.user, ) - assign_perm("view_document", self.user2, self.d2) - assign_perm("view_document", self.group1, self.d3) + grant_object(self.user2, self.d2, "view_document") + grant_object(self.group1, self.d3, "view_document") self.t1 = Tag.objects.create(name="t") self.dt1 = DocumentType.objects.create(name="dt") diff --git a/src/documents/tests/test_merge_documents_as_versions.py b/src/documents/tests/test_merge_documents_as_versions.py index 7e2457965..bbe4b04cc 100644 --- a/src/documents/tests/test_merge_documents_as_versions.py +++ b/src/documents/tests/test_merge_documents_as_versions.py @@ -2,7 +2,6 @@ import json from unittest import mock from auditlog.models import LogEntry -from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.test import TestCase from rest_framework import status @@ -12,6 +11,7 @@ from documents.bulk_edit import merge_as_versions from documents.models import Document from documents.serialisers import MergeDocumentsAsVersionsSerializer from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global class TestMergeDocumentsAsVersionsSerializer(TestCase): @@ -336,11 +336,7 @@ class TestMergeDocumentsAsVersions(TestCase): class TestMergeDocumentsAsVersionsAPI(APITestCase): def setUp(self) -> None: self.user = UserFactory(username="user") - self.user.user_permissions.add( - Permission.objects.get(codename="change_document"), - Permission.objects.get(codename="view_document"), - Permission.objects.get(codename="delete_document"), - ) + grant_global(self.user, "change_document", "view_document", "delete_document") self.doc1 = Document.objects.create( checksum="A", title="A", @@ -406,10 +402,7 @@ class TestMergeDocumentsAsVersionsAPI(APITestCase): merge_mock.__name__ = "merge_as_versions" # Owns them and may change them, but may not make them stop being documents user = UserFactory(username="no-delete") - user.user_permissions.add( - Permission.objects.get(codename="change_document"), - Permission.objects.get(codename="view_document"), - ) + grant_global(user, "change_document", "view_document") for doc in (self.doc1, self.doc2): doc.owner = user doc.save() diff --git a/src/documents/tests/test_permission_filtering_security.py b/src/documents/tests/test_permission_filtering_security.py index d1852d89d..018d5bb94 100644 --- a/src/documents/tests/test_permission_filtering_security.py +++ b/src/documents/tests/test_permission_filtering_security.py @@ -7,9 +7,7 @@ from unittest.mock import patch import pytest from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission from django.test import override_settings -from guardian.shortcuts import assign_perm from rest_framework.test import APIClient from documents.matching import match_correspondents @@ -30,6 +28,8 @@ from paperless_testing.factories import DocumentTypeFactory from paperless_testing.factories import StoragePathFactory from paperless_testing.factories import TagFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object if TYPE_CHECKING: from paperless_testing.dirs import PaperlessDirs @@ -80,7 +80,7 @@ class TestPermittedDocumentIdsSecurity: owner = UserFactory(username="owner") shared = DocumentFactory(owner=owner) not_shared = DocumentFactory(owner=owner) - assign_perm("view_document", grantee, shared) + grant_object(grantee, shared, "view_document") assert_visible_document_ids( permitted_document_ids(grantee), @@ -100,7 +100,7 @@ class TestPermittedDocumentIdsSecurity: group = Group.objects.create(name="finance") member.groups.add(group) shared = DocumentFactory(owner=owner) - assign_perm("view_document", group, shared) + grant_object(group, shared, "view_document") assert_visible_document_ids( permitted_document_ids(member), @@ -197,12 +197,10 @@ class TestAiChatAllDocumentsPermissionBoundary: owner = UserFactory(username="owner") asker = UserFactory(username="asker") - asker.user_permissions.add( - *Permission.objects.filter(codename="view_document"), - ) + grant_global(asker, "view_document") shared = DocumentFactory(owner=owner) not_shared = DocumentFactory(owner=owner) - assign_perm("view_document", asker, shared) + grant_object(asker, shared, "view_document") client = APIClient() client.force_authenticate(user=asker) @@ -229,7 +227,7 @@ class TestDuplicateDocumentsPermissionBoundary: dup_visible = DocumentFactory(owner=owner, checksum="dupe-checksum") dup_hidden = DocumentFactory(owner=owner, checksum="dupe-checksum") dup_hidden.delete() # soft delete, should still be found (include_deleted=True) - assign_perm("view_document", stranger, dup_visible) + grant_object(stranger, dup_visible, "view_document") result_owner = _get_viewable_duplicates(original, owner) assert {d.pk for d in result_owner} == {dup_visible.pk, dup_hidden.pk} @@ -245,9 +243,9 @@ class TestPermittedDocumentIdsArbitraryPermission: viewer_only = UserFactory(username="viewer") editor = UserFactory(username="editor") doc = DocumentFactory(owner=owner) - assign_perm("view_document", viewer_only, doc) - assign_perm("change_document", editor, doc) - assign_perm("view_document", editor, doc) + grant_object(viewer_only, doc, "view_document") + grant_object(editor, doc, "change_document") + grant_object(editor, doc, "view_document") assert_visible_document_ids( permitted_document_ids(editor, perm="change_document"), @@ -264,7 +262,7 @@ class TestPermittedDocumentIdsArbitraryPermission: owner = UserFactory(username="owner") editor = UserFactory(username="editor") doc = DocumentFactory(owner=owner) - assign_perm("change_document", editor, doc) + grant_object(editor, doc, "change_document") assert_visible_document_ids( permitted_document_ids(editor, perm="documents.change_document"), @@ -277,7 +275,7 @@ class TestPermittedDocumentIdsArbitraryPermission: stranger = UserFactory(username="mallory") view_only = UserFactory(username="viewer") doc = DocumentFactory(owner=owner) - assign_perm("view_document", view_only, doc) + grant_object(view_only, doc, "view_document") doc.delete() assert_visible_document_ids( @@ -313,9 +311,7 @@ class TestEmailDocumentPermissionBoundary: ): owner = UserFactory(username="owner") requester = UserFactory(username="requester") - requester.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(requester, "view_document") rest_api_client.force_authenticate(user=requester) hidden = DocumentFactory(owner=owner) @@ -347,15 +343,13 @@ class TestBulkEditChangePermissionBoundary: requester = UserFactory(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"), - ) + grant_global(requester, "change_document") rest_api_client.force_authenticate(user=requester) changeable = DocumentFactory(owner=owner) - assign_perm("view_document", requester, changeable) - assign_perm("change_document", requester, changeable) # fully permitted + grant_object(requester, changeable, "view_document") + grant_object(requester, changeable, "change_document") # fully permitted target = DocumentFactory(owner=owner) - assign_perm("view_document", requester, target) # view only, NOT change + grant_object(requester, target, "view_document") # view only, NOT change response = rest_api_client.post( "/api/documents/bulk_edit/", @@ -380,7 +374,7 @@ class TestBulkDownloadPermissionChecksRootDocument: requester = UserFactory(username="requester") root = DocumentFactory(owner=owner) root.source_path.write_bytes(b"%PDF-1.4 test") - assign_perm("view_document", requester, root) + grant_object(requester, root, "view_document") rest_api_client.force_authenticate(user=requester) response = rest_api_client.post( @@ -398,15 +392,13 @@ class TestBulkDownloadPermissionChecksRootDocument: ) -> None: owner = UserFactory(username="owner") requester = UserFactory(username="requester") - requester.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(requester, "view_document") 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 + grant_object(requester, root, "view_document") # granted on ROOT only response = rest_api_client.post( "/api/documents/bulk_download/", @@ -425,10 +417,8 @@ class TestBulkDownloadPermissionChecksRootDocument: # `stranger` case) can't tell the two apart, since they're denied # either way. version_only_grantee = UserFactory(username="version_only_grantee") - version_only_grantee.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) - assign_perm("view_document", version_only_grantee, version) + grant_global(version_only_grantee, "view_document") + grant_object(version_only_grantee, version, "view_document") rest_api_client.force_authenticate(user=version_only_grantee) response = rest_api_client.post( "/api/documents/bulk_download/", @@ -449,12 +439,10 @@ class TestTrashRestorePermissionBoundary: ): owner = UserFactory(username="owner") requester = UserFactory(username="requester") - requester.user_permissions.add( - Permission.objects.get(codename="delete_document"), - ) + grant_global(requester, "delete_document") rest_api_client.force_authenticate(user=requester) doc = DocumentFactory(owner=owner) - assign_perm("view_document", requester, doc) # view only, NOT delete + grant_object(requester, doc, "view_document") # view only, NOT delete doc.delete() response = rest_api_client.post( @@ -470,12 +458,10 @@ class TestTrashRestorePermissionBoundary: ): owner = UserFactory(username="owner") requester = UserFactory(username="requester") - requester.user_permissions.add( - Permission.objects.get(codename="delete_document"), - ) + grant_global(requester, "delete_document") rest_api_client.force_authenticate(user=requester) doc = DocumentFactory(owner=owner) - assign_perm("delete_document", requester, doc) + grant_object(requester, doc, "delete_document") doc.delete() response = rest_api_client.post( @@ -490,7 +476,7 @@ class TestTrashRestorePermissionBoundary: requester = UserFactory(username="requester") rest_api_client.force_authenticate(user=requester) doc = DocumentFactory(owner=owner) - assign_perm("delete_document", requester, doc) + grant_object(requester, doc, "delete_document") doc.delete() response = rest_api_client.post( @@ -517,12 +503,10 @@ class TestTrashViewExcludesExplicitlyGrantedDocuments: def test_explicit_grant_does_not_leak_trashed_document(self, rest_api_client): owner = UserFactory(username="trash_owner") grantee = UserFactory(username="trash_grantee") - grantee.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(grantee, "view_document") doc = DocumentFactory(owner=owner) doc.delete() # soft delete - assign_perm("view_document", grantee, doc) + grant_object(grantee, doc, "view_document") rest_api_client.force_authenticate(user=grantee) response = rest_api_client.get("/api/trash/") @@ -565,7 +549,7 @@ class TestPermittedObjectIdsGenericModels: ) other = UserFactory(username=f"other_{suffix}") granted = factory(owner=other) - assign_perm(perm, user, granted) + grant_object(user, granted, perm) assert_visible_document_ids( permitted_object_ids(user, model, perm), @@ -593,7 +577,7 @@ class TestPermittedObjectIdsGenericModels: stranger = UserFactory(username=f"stranger2_{model.__name__}") shared = factory(owner=owner) not_shared = factory(owner=owner) - assign_perm(perm, grantee, shared) + grant_object(grantee, shared, perm) assert_visible_document_ids( permitted_object_ids(grantee, model, perm), @@ -618,7 +602,7 @@ class TestPermittedObjectIdsGenericModels: group = Group.objects.create(name=f"group_{model.__name__}") member.groups.add(group) shared = factory(owner=owner) - assign_perm(perm, group, shared) + grant_object(group, shared, perm) assert_visible_document_ids( permitted_object_ids(member, model, perm), @@ -658,7 +642,7 @@ class TestMatchingRespectsObjectPermissions: match="invoice", matching_algorithm=Tag.MATCH_LITERAL, ) - assign_perm("view_tag", classifying_user, visible_tag) + grant_object(classifying_user, visible_tag, "view_tag") doc = DocumentFactory(owner=classifying_user, content="an invoice document") matched = match_tags(doc, classifier=None, user=classifying_user) @@ -679,7 +663,7 @@ class TestMatchingRespectsObjectPermissions: match="invoice", matching_algorithm=Correspondent.MATCH_LITERAL, ) - assign_perm("view_correspondent", classifying_user, visible_correspondent) + grant_object(classifying_user, visible_correspondent, "view_correspondent") doc = DocumentFactory(owner=classifying_user, content="an invoice document") matched = match_correspondents(doc, classifier=None, user=classifying_user) @@ -700,7 +684,7 @@ class TestMatchingRespectsObjectPermissions: match="invoice", matching_algorithm=DocumentType.MATCH_LITERAL, ) - assign_perm("view_documenttype", classifying_user, visible_document_type) + grant_object(classifying_user, visible_document_type, "view_documenttype") doc = DocumentFactory(owner=classifying_user, content="an invoice document") matched = match_document_types(doc, classifier=None, user=classifying_user) @@ -721,7 +705,7 @@ class TestMatchingRespectsObjectPermissions: match="invoice", matching_algorithm=StoragePath.MATCH_LITERAL, ) - assign_perm("view_storagepath", classifying_user, visible_storage_path) + grant_object(classifying_user, visible_storage_path, "view_storagepath") doc = DocumentFactory(owner=classifying_user, content="an invoice document") matched = match_storage_paths(doc, classifier=None, user=classifying_user) @@ -738,9 +722,7 @@ class TestBulkEditObjectsApplyToAllPermissionBoundary: new_owner = UserFactory(username="tags_new_owner") # grant the global change_tag permission so the object-level # filtering (not the global has_perm check) is what's under test - requester.user_permissions.add( - Permission.objects.get(codename="change_tag"), - ) + grant_global(requester, "change_tag") rest_api_client.force_authenticate(user=requester) visible = TagFactory(owner=requester) hidden = TagFactory(owner=owner) @@ -775,14 +757,12 @@ class TestBulkEditObjectsApplyToAllPermissionBoundary: """ owner = UserFactory(username="shared_tags_owner") requester = UserFactory(username="shared_tags_requester") - requester.user_permissions.add( - Permission.objects.get(codename="change_tag"), - ) + grant_global(requester, "change_tag") rest_api_client.force_authenticate(user=requester) owned = TagFactory(owner=requester) shared = TagFactory(owner=owner) - assign_perm("view_tag", requester, shared) - assign_perm("change_tag", requester, shared) + grant_object(requester, shared, "view_tag") + grant_object(requester, shared, "change_tag") response = rest_api_client.post( "/api/bulk_edit_objects/", @@ -838,9 +818,7 @@ class TestBulkEditObjectsTagDescendantPartialPermission: new_owner = UserFactory(username="tag_hierarchy_new_owner") # global change_tag permission so the has_perm() gate passes and the # object-level permitted_object_ids filtering is what's under test - requester.user_permissions.add( - Permission.objects.get(codename="change_tag"), - ) + grant_global(requester, "change_tag") rest_api_client.force_authenticate(user=requester) parent = TagFactory(owner=requester, name="parent-tag") diff --git a/src/documents/tests/test_permitted_objects_filter.py b/src/documents/tests/test_permitted_objects_filter.py index 1f1348629..80403252e 100644 --- a/src/documents/tests/test_permitted_objects_filter.py +++ b/src/documents/tests/test_permitted_objects_filter.py @@ -1,11 +1,11 @@ import pytest -from guardian.shortcuts import assign_perm from rest_framework.test import APIRequestFactory from documents.filters import PermittedObjectsFilter from documents.models import Tag from paperless_testing.factories import TagFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_object class _DummyView: @@ -35,7 +35,7 @@ class TestPermittedObjectsFilter: unowned = TagFactory(owner=None) granted = TagFactory(owner=owner) hidden = TagFactory(owner=owner) - assign_perm("view_tag", grantee, granted) + grant_object(grantee, granted, "view_tag") request = APIRequestFactory().get("/") request.user = grantee @@ -53,7 +53,7 @@ class TestPermittedObjectsFilter: grantee = UserFactory(username="grantee2") owned = TagFactory(owner=grantee) granted = TagFactory(owner=owner) - assign_perm("view_tag", grantee, granted) + grant_object(grantee, granted, "view_tag") request = APIRequestFactory().get("/") request.user = grantee @@ -82,7 +82,7 @@ class TestPermittedObjectsFilter: TagFactory(owner=None) TagFactory(owner=user) granted = TagFactory(owner=UserFactory(username=f"o_{username}")) - assign_perm("view_tag", user, granted) + grant_object(user, granted, "view_tag") request = APIRequestFactory().get("/") request.user = user diff --git a/src/documents/tests/test_share_link_bundles.py b/src/documents/tests/test_share_link_bundles.py index 96e35415a..f451e58ba 100644 --- a/src/documents/tests/test_share_link_bundles.py +++ b/src/documents/tests/test_share_link_bundles.py @@ -6,10 +6,8 @@ from pathlib import Path from unittest import mock from django.conf import settings -from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.utils import timezone -from guardian.shortcuts import assign_perm from rest_framework import serializers from rest_framework import status from rest_framework.test import APITestCase @@ -23,6 +21,8 @@ from documents.tasks import cleanup_expired_share_link_bundles from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import DocumentFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): @@ -58,11 +58,9 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): ) -> None: owner = UserFactory(username="document_owner") requester = UserFactory(username="bundle_creator") - requester.user_permissions.add( - Permission.objects.get(codename="add_sharelinkbundle"), - ) + grant_global(requester, "add_sharelinkbundle") document = DocumentFactory.create(owner=owner) - assign_perm("view_document", requester, document) + grant_object(requester, document, "view_document") self.client.force_authenticate(requester) payload = { "document_ids": [document.pk], @@ -73,9 +71,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): response = self.client.post(self.ENDPOINT, payload, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - requester.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(requester, "view_document") requester = User.objects.get(pk=requester.pk) self.client.force_authenticate(requester) response = self.client.post(self.ENDPOINT, payload, format="json") diff --git a/src/documents/tests/test_tag_hierarchy.py b/src/documents/tests/test_tag_hierarchy.py index 9b876b982..5384fd187 100644 --- a/src/documents/tests/test_tag_hierarchy.py +++ b/src/documents/tests/test_tag_hierarchy.py @@ -1,6 +1,5 @@ from unittest import mock -from django.contrib.auth.models import Permission from rest_framework import status from rest_framework.test import APITestCase @@ -14,15 +13,14 @@ from documents.serialisers import TagSerializer from documents.signals.handlers import run_workflows from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global class TestTagHierarchyPermissions(APITestCase): def test_children_only_include_visible_tags(self) -> None: owner = UserFactory(username="owner") requester = UserFactory(username="requester") - requester.user_permissions.add( - Permission.objects.get(codename="view_tag"), - ) + grant_global(requester, "view_tag") parent = Tag.objects.create(name="Visible parent", owner=requester) hidden_child = Tag.objects.create( name="Hidden child", diff --git a/src/documents/tests/test_views.py b/src/documents/tests/test_views.py index 90b3a578c..c20d247d1 100644 --- a/src/documents/tests/test_views.py +++ b/src/documents/tests/test_views.py @@ -15,7 +15,6 @@ from django.test import TestCase from django.test import override_settings from django.test.utils import CaptureQueriesContext from django.utils import timezone -from guardian.shortcuts import assign_perm from rest_framework import status from documents.caching import get_llm_suggestion_cache @@ -35,6 +34,8 @@ from paperless_ai.exceptions import LLMProviderError from paperless_ai.exceptions import LLMTimeoutError from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestViews(DirectoriesMixin, TestCase): @@ -142,9 +143,7 @@ class TestViews(DirectoriesMixin, TestCase): codename__contains="sharelink", ) self.user.user_permissions.add(*sharelink_permissions) - self.user.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(self.user, "view_document") self.user.save() self.client.force_login(self.user) @@ -206,9 +205,7 @@ class TestViews(DirectoriesMixin, TestCase): codename__contains="sharelink", ) self.user.user_permissions.add(*sharelink_permissions) - self.user.user_permissions.add( - Permission.objects.get(codename="view_document"), - ) + grant_global(self.user, "view_document") self.client.force_login(self.user) create_response = self.client.post( @@ -241,16 +238,16 @@ class TestViews(DirectoriesMixin, TestCase): group2 = Group.objects.create(name="group2") group3 = Group.objects.create(name="group3") t1 = Tag.objects.create(name="invoice", pk=1) - assign_perm("view_tag", self.user, t1) - assign_perm("view_tag", user2, t1) - assign_perm("view_tag", user3, t1) - assign_perm("view_tag", group1, t1) - assign_perm("view_tag", group2, t1) - assign_perm("view_tag", group3, t1) - assign_perm("change_tag", self.user, t1) - assign_perm("change_tag", user2, t1) - assign_perm("change_tag", group1, t1) - assign_perm("change_tag", group2, t1) + grant_object(self.user, t1, "view_tag") + grant_object(user2, t1, "view_tag") + grant_object(user3, t1, "view_tag") + grant_object(group1, t1, "view_tag") + grant_object(group2, t1, "view_tag") + grant_object(group3, t1, "view_tag") + grant_object(self.user, t1, "change_tag") + grant_object(user2, t1, "change_tag") + grant_object(group1, t1, "change_tag") + grant_object(group2, t1, "change_tag") Tag.objects.create(name="bank statement", pk=2) d1 = Document.objects.create( @@ -429,11 +426,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): tag_owner = UserFactory(username="cache_tag_owner") invisible_tag = Tag.objects.create(name="cache_restricted", owner=tag_owner) requester = UserFactory(username="cache_requester") - requester.user_permissions.add( - *Permission.objects.filter( - codename__in=["view_document", "change_document", "view_tag"], - ), - ) + grant_global(requester, "view_document", "change_document", "view_tag") mock_get_cache.return_value = MagicMock( suggestions={ "title": "Untitled", @@ -879,11 +872,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): tag_owner = UserFactory(username="tagowner") invisible_tag = Tag.objects.create(name="restricted", owner=tag_owner) requester = UserFactory(username="requester") - requester.user_permissions.add( - *Permission.objects.filter( - codename__in=["view_document", "change_document", "view_tag"], - ), - ) + grant_global(requester, "view_document", "change_document", "view_tag") mock_get_ai_classification.return_value = { "title": "Untitled", @@ -967,9 +956,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase): super().setUp() def grant_view_document_permission(self) -> None: - self.user.user_permissions.add( - *Permission.objects.filter(codename="view_document"), - ) + grant_global(self.user, "view_document") @override_settings(AI_ENABLED=False) def test_post_ai_disabled(self) -> None: diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 6c0640746..b20e81880 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -17,7 +17,6 @@ from django.contrib.auth.models import User from django.core import mail from django.test import override_settings from django.utils import timezone -from guardian.shortcuts import assign_perm from guardian.shortcuts import get_groups_with_perms from guardian.shortcuts import get_users_with_perms from httpx import ConnectError @@ -72,6 +71,7 @@ from paperless_mail.models import MailAccount from paperless_mail.models import MailRule from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_object class TestWorkflows( @@ -2130,10 +2130,10 @@ class TestWorkflows( original_filename="sample.pdf", ) - assign_perm("documents.view_document", self.user2, doc) - assign_perm("documents.change_document", self.user2, doc) - assign_perm("documents.view_document", self.group1, doc) - assign_perm("documents.change_document", self.group1, doc) + grant_object(self.user2, doc, "documents.view_document") + grant_object(self.user2, doc, "documents.change_document") + grant_object(self.group1, doc, "documents.view_document") + grant_object(self.group1, doc, "documents.change_document") superuser = UserFactory(username="superuser", superuser=True) self.client.force_authenticate(user=superuser) @@ -2901,10 +2901,10 @@ class TestWorkflows( doc.tags.set([self.t1, self.t2]) CustomFieldInstance.objects.create(document=doc, field=self.cf1) doc.save() - assign_perm("documents.view_document", self.user3, doc) - assign_perm("documents.change_document", self.user3, doc) - assign_perm("documents.view_document", self.group1, doc) - assign_perm("documents.change_document", self.group1, doc) + grant_object(self.user3, doc, "documents.view_document") + grant_object(self.user3, doc, "documents.change_document") + grant_object(self.group1, doc, "documents.view_document") + grant_object(self.group1, doc, "documents.change_document") superuser = UserFactory(username="superuser", superuser=True) self.client.force_authenticate(user=superuser) @@ -3098,10 +3098,10 @@ class TestWorkflows( doc.tags.set([self.t1, self.t2]) CustomFieldInstance.objects.create(document=doc, field=self.cf1) doc.save() - assign_perm("documents.view_document", self.user3, doc) - assign_perm("documents.change_document", self.user3, doc) - assign_perm("documents.view_document", self.group1, doc) - assign_perm("documents.change_document", self.group1, doc) + grant_object(self.user3, doc, "documents.view_document") + grant_object(self.user3, doc, "documents.change_document") + grant_object(self.group1, doc, "documents.view_document") + grant_object(self.group1, doc, "documents.change_document") superuser = UserFactory(username="superuser", superuser=True) self.client.force_authenticate(user=superuser) diff --git a/src/paperless_ai/tests/test_ai_classifier.py b/src/paperless_ai/tests/test_ai_classifier.py index 08940f883..cbdd1e7f1 100644 --- a/src/paperless_ai/tests/test_ai_classifier.py +++ b/src/paperless_ai/tests/test_ai_classifier.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest import pytest_mock from django.test import override_settings -from guardian.shortcuts import assign_perm from guardian.shortcuts import remove_perm from documents.models import Document @@ -27,6 +26,7 @@ from paperless_ai.taxonomy import TaxonomyCandidates from paperless_testing.factories import DocumentFactory from paperless_testing.factories import TagFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_object @pytest.fixture @@ -815,8 +815,8 @@ class TestFulltextSimilarDocuments: content="shared content phrase", owner=owner, ) - assign_perm("view_document", viewer, permitted) - assign_perm("view_document", viewer, now_private) + grant_object(viewer, permitted, "view_document") + grant_object(viewer, now_private, "view_document") fulltext_backend.add_or_update(source) fulltext_backend.add_or_update(permitted) fulltext_backend.add_or_update(now_private) diff --git a/src/paperless_mail/tests/test_api.py b/src/paperless_mail/tests/test_api.py index ca331624b..bb75bc6c6 100644 --- a/src/paperless_mail/tests/test_api.py +++ b/src/paperless_mail/tests/test_api.py @@ -1,9 +1,7 @@ import json from unittest import mock -from django.contrib.auth.models import Permission from django.contrib.auth.models import User -from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase @@ -19,6 +17,9 @@ from paperless_testing.factories import CorrespondentFactory from paperless_testing.factories import DocumentTypeFactory from paperless_testing.factories import TagFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_all_global +from paperless_testing.permissions import grant_global +from paperless_testing.permissions import grant_object class TestAPIMailAccounts(DirectoriesMixin, APITestCase): @@ -35,7 +36,7 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase): super().setUp() self.user = UserFactory(username="temp_admin") - self.user.user_permissions.add(*Permission.objects.all()) + grant_all_global(self.user) self.user.save() self.client.force_authenticate(user=self.user) @@ -331,7 +332,7 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase): owner=owner, ) user = UserFactory(username="object_perms_only") - assign_perm("change_mailaccount", user, account) + grant_object(user, account, "change_mailaccount") self.client.force_authenticate(user=user) response = self.client.post( @@ -384,7 +385,7 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase): account2 = MailAccountFactory(name="Email2", owner=self.user) _account3 = MailAccountFactory(name="Email3", owner=user2) account4 = MailAccountFactory(name="Email4", owner=user2) - assign_perm("view_mailaccount", self.user, account4) + grant_object(self.user, account4, "view_mailaccount") response = self.client.get(self.ENDPOINT) @@ -402,7 +403,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): super().setUp() self.user = UserFactory(username="temp_admin") - self.user.user_permissions.add(*Permission.objects.all()) + grant_all_global(self.user) self.user.save() self.client.force_authenticate(user=self.user) @@ -636,7 +637,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): ) -> None: other_user = UserFactory(username="mail-owner") foreign_account = MailAccountFactory(name="ForeignEmail", owner=other_user) - assign_perm("change_mailaccount", self.user, foreign_account) + grant_object(self.user, foreign_account, "change_mailaccount") response = self.client.post( self.ENDPOINT, @@ -688,7 +689,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): rule2 = MailRuleFactory(account=account1, order=1, owner=self.user) MailRuleFactory(account=account1, order=2, owner=user2) rule4 = MailRuleFactory(account=account1, order=3, owner=user2) - assign_perm("view_mailrule", self.user, rule4) + grant_object(self.user, rule4, "view_mailrule") response = self.client.get(self.ENDPOINT) @@ -739,7 +740,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase): super().setUp() self.user = UserFactory(username="temp_admin") - self.user.user_permissions.add(*Permission.objects.all()) + grant_all_global(self.user) self.user.save() self.client.force_authenticate(user=self.user) @@ -763,7 +764,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase): ) ProcessedMailFactory(rule=rule, owner=user2) pm4 = ProcessedMailFactory(rule=rule, owner=user2) - assign_perm("view_processedmail", self.user, pm4) + grant_object(self.user, pm4, "view_processedmail") response = self.client.get(self.ENDPOINT) @@ -814,7 +815,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase): owner=self.user, ) pm_granted = ProcessedMailFactory(rule=rule, owner=user2) - assign_perm("delete_processedmail", self.user, pm_granted) + grant_object(self.user, pm_granted, "delete_processedmail") pm_forbidden = ProcessedMailFactory(rule=rule, owner=user2) # Success for allowed items @@ -858,11 +859,9 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase): def test_bulk_delete_requires_global_delete_permission(self) -> None: owner = UserFactory(username="mail_owner") requester = UserFactory(username="mail_deleter") - requester.user_permissions.add( - Permission.objects.get(codename="add_processedmail"), - ) + grant_global(requester, "add_processedmail") mail = ProcessedMailFactory(owner=owner) - assign_perm("delete_processedmail", requester, mail) + grant_object(requester, mail, "delete_processedmail") self.client.force_authenticate(requester) response = self.client.post( @@ -872,9 +871,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - requester.user_permissions.add( - Permission.objects.get(codename="delete_processedmail"), - ) + grant_global(requester, "delete_processedmail") requester = User.objects.get(pk=requester.pk) self.client.force_authenticate(requester) response = self.client.post( diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 14882a6a1..62bcabd7c 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -43,6 +43,7 @@ from paperless_mail.tests.factories import MailRuleFactory from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import CorrespondentFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global @dataclasses.dataclass @@ -2264,11 +2265,7 @@ class TestMailAccountTestView(APITestCase): self.mailMocker.setUp() self.addCleanup(self.mailMocker.doCleanups) self.user = UserFactory(username="testuser", password="testpassword") - self.user.user_permissions.add( - *Permission.objects.filter( - codename__in=["add_mailaccount", "change_mailaccount"], - ), - ) + grant_global(self.user, "add_mailaccount", "change_mailaccount") self.user.save() self.client.force_authenticate(user=self.user) self.url = "/api/mail_accounts/test/" diff --git a/src/paperless_mail/tests/test_mail_oauth.py b/src/paperless_mail/tests/test_mail_oauth.py index 26765b7da..c3ba99ce9 100644 --- a/src/paperless_mail/tests/test_mail_oauth.py +++ b/src/paperless_mail/tests/test_mail_oauth.py @@ -14,6 +14,7 @@ from paperless_mail.models import MailAccount from paperless_mail.oauth import PaperlessMailOAuth2Manager from paperless_mail.tests.factories import MailAccountFactory from paperless_testing.factories import UserFactory +from paperless_testing.permissions import grant_global @override_settings( @@ -28,13 +29,7 @@ class TestMailOAuth( ): def setUp(self) -> None: self.user = UserFactory(username="testuser") - self.user.user_permissions.add( - *Permission.objects.filter( - codename__in=[ - "add_mailaccount", - ], - ), - ) + grant_global(self.user, "add_mailaccount") self.user.save() self.client.force_login(self.user) self.mail_account_handler = MailAccountHandler() diff --git a/src/paperless_testing/permissions.py b/src/paperless_testing/permissions.py new file mode 100644 index 000000000..2cda83b23 --- /dev/null +++ b/src/paperless_testing/permissions.py @@ -0,0 +1,43 @@ +"""Helpers that grant permissions to users and groups in tests. + +Two mechanisms, two names. A global permission is a Django model permission held +by a user. An object permission is a guardian permission held on one object by a +user or a group. They are not interchangeable. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from django.contrib.auth.models import Permission +from guardian.shortcuts import assign_perm + +if TYPE_CHECKING: + from django.contrib.auth.models import Group + from django.contrib.auth.models import User + from django.db.models import Model + + +def grant_global(user: User, *perms: str) -> None: + """Grant model-level permissions. + + Each of perms is a codename or "app_label.codename". An unknown name raises + Permission.DoesNotExist. + """ + for perm in perms: + app_label, _, codename = perm.rpartition(".") + lookup = {"codename": codename} + if app_label: + lookup["content_type__app_label"] = app_label + user.user_permissions.add(Permission.objects.get(**lookup)) + + +def grant_all_global(user: User) -> None: + """Grant every existing model-level permission without making a superuser.""" + user.user_permissions.add(*Permission.objects.all()) + + +def grant_object(target: User | Group, obj: Model, *perms: str) -> None: + """Grant object-level permissions on obj to a user or a group.""" + for perm in perms: + assign_perm(perm, target, obj)