diff --git a/src/documents/permissions.py b/src/documents/permissions.py index d297c43744..9a48e3a809 100644 --- a/src/documents/permissions.py +++ b/src/documents/permissions.py @@ -31,6 +31,7 @@ from guardian.core import ObjectPermissionChecker from guardian.models import GroupObjectPermission from guardian.models import UserObjectPermission from rest_framework.permissions import BasePermission +from rest_framework.permissions import DjangoModelPermissions from rest_framework.permissions import DjangoObjectPermissions from documents.models import Document @@ -63,6 +64,17 @@ class PermissionGranteesUpdate(TypedDict, total=False): PermissionsSpec: TypeAlias = Mapping[PermissionAction, PermissionGranteesUpdate] +class PaperlessModelPermissions(DjangoModelPermissions): + """Require basic global model permissions.""" + + perms_map = { + **DjangoModelPermissions.perms_map, + "GET": ["%(app_label)s.view_%(model_name)s"], + "OPTIONS": ["%(app_label)s.view_%(model_name)s"], + "HEAD": ["%(app_label)s.view_%(model_name)s"], + } + + class PaperlessObjectPermissions(DjangoObjectPermissions): """ A permissions backend that checks for object-level permissions diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index b06c6f67d0..109a196619 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -15,6 +15,7 @@ from paperless.models import ColorConvertChoices from paperless_testing.dirs import DirectoriesMixin from paperless_testing.factories import UserFactory from paperless_testing.http import read_streaming_response +from paperless_testing.permissions import grant_global class TestApiAppConfig(DirectoriesMixin, APITestCase): @@ -26,6 +27,67 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): user = UserFactory(username="temp_admin", superuser=True) self.client.force_authenticate(user=user) + def test_api_read_config_requires_view_permission(self) -> None: + for permissions in ( + (), + ("change_applicationconfiguration",), + ("view_document",), + ): + user = UserFactory() + grant_global(user, *permissions) + self.client.force_authenticate(user=user) + + for endpoint in (self.ENDPOINT, f"{self.ENDPOINT}1/"): + for method in ("get", "head", "options"): + with self.subTest( + permissions=permissions, + endpoint=endpoint, + method=method, + ): + response = getattr(self.client, method)(endpoint) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN, + ) + + def test_api_read_config_with_view_permission(self) -> None: + user = UserFactory() + grant_global(user, "view_applicationconfiguration") + self.client.force_authenticate(user=user) + + for endpoint in (self.ENDPOINT, f"{self.ENDPOINT}1/"): + for method in ("get", "head", "options"): + with self.subTest(endpoint=endpoint, method=method): + response = getattr(self.client, method)(endpoint) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_api_view_config_permission_does_not_allow_writes(self) -> None: + user = UserFactory() + grant_global(user, "view_applicationconfiguration") + self.client.force_authenticate(user=user) + + for method in ("put", "patch", "delete"): + with self.subTest(method=method): + response = getattr(self.client, method)(f"{self.ENDPOINT}1/") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_api_change_config_permission_allows_updates(self) -> None: + user = UserFactory() + grant_global(user, "change_applicationconfiguration") + self.client.force_authenticate(user=user) + + response = self.client.patch( + f"{self.ENDPOINT}1/", + {"app_title": "Updated title"}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + ApplicationConfiguration.objects.get().app_title, + "Updated title", + ) + def test_api_get_config(self) -> None: """ GIVEN: diff --git a/src/paperless/views.py b/src/paperless/views.py index 62b6242944..cfb1d7ebbb 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -32,13 +32,13 @@ from rest_framework.fields import BooleanField from rest_framework.filters import OrderingFilter from rest_framework.generics import GenericAPIView from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import DjangoModelPermissions from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.throttling import ScopedRateThrottle from rest_framework.viewsets import ModelViewSet from documents.models import PaperlessTask +from documents.permissions import PaperlessModelPermissions from documents.permissions import PaperlessObjectPermissions from documents.tasks import llmindex_index from paperless.filters import GroupFilterSet @@ -414,7 +414,7 @@ class ApplicationConfigurationViewSet(ModelViewSet[ApplicationConfiguration]): queryset = ApplicationConfiguration.objects serializer_class = ApplicationConfigurationSerializer - permission_classes = (IsAuthenticated, DjangoModelPermissions) + permission_classes = (IsAuthenticated, PaperlessModelPermissions) @extend_schema(exclude=True) def create(self, request, *args, **kwargs):