From fad13c148eafb033afd8e5f88ef030156b5263b6 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 5 Dec 2022 22:56:03 -0800 Subject: [PATCH] Object-level permissions + filtering --- src/documents/filters.py | 15 +++++++++++++++ src/documents/permissions.py | 19 +++++++++++++++---- src/documents/views.py | 21 +++++++++++++-------- src/paperless/settings.py | 11 ++++++----- src/paperless/views.py | 6 +++--- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/documents/filters.py b/src/documents/filters.py index d66849e00..4bcbda98d 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -2,6 +2,7 @@ from django.db.models import Q from django_filters.rest_framework import BooleanFilter from django_filters.rest_framework import Filter from django_filters.rest_framework import FilterSet +from rest_framework_guardian.filters import ObjectPermissionsFilter from .models import Correspondent from .models import Document @@ -134,3 +135,17 @@ class StoragePathFilterSet(FilterSet): "name": CHAR_KWARGS, "path": CHAR_KWARGS, } + + +class ObjectOwnedOrGrandtedPermissionsFilter(ObjectPermissionsFilter): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions, owns the objects, or objects without + an owner (for backwards compat) + """ + + def filter_queryset(self, request, queryset, view): + objects_with_perms = super().filter_queryset(request, queryset, view) + objects_owned = queryset.filter(owner=request.user) + objects_unowned = queryset.filter(owner__isnull=True) + return objects_with_perms | objects_owned | objects_unowned diff --git a/src/documents/permissions.py b/src/documents/permissions.py index 86cc66c18..43b620b02 100644 --- a/src/documents/permissions.py +++ b/src/documents/permissions.py @@ -1,18 +1,29 @@ from rest_framework.permissions import BasePermission -from rest_framework.permissions import DjangoModelPermissions +from rest_framework.permissions import DjangoObjectPermissions -class PaperlessModelPermissions(DjangoModelPermissions): +class PaperlessObjectPermissions(DjangoObjectPermissions): + """ + A permissions backend that checks for object-level permissions + or for ownership. + """ + perms_map = { "GET": ["%(app_label)s.view_%(model_name)s"], - "OPTIONS": [], - "HEAD": [], + "OPTIONS": ["%(app_label)s.view_%(model_name)s"], + "HEAD": ["%(app_label)s.view_%(model_name)s"], "POST": ["%(app_label)s.add_%(model_name)s"], "PUT": ["%(app_label)s.change_%(model_name)s"], "PATCH": ["%(app_label)s.change_%(model_name)s"], "DELETE": ["%(app_label)s.delete_%(model_name)s"], } + def has_object_permission(self, request, view, obj): + if hasattr(obj, "owner") and request.user == obj.owner: + return True + else: + return super().has_object_permission(request, view, obj) + class PaperlessAdminPermissions(BasePermission): def has_permission(self, request, view): diff --git a/src/documents/views.py b/src/documents/views.py index 0657ae93a..2a8881376 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -28,8 +28,9 @@ from django.utils.translation import get_language from django.views.decorators.cache import cache_control from django.views.generic import TemplateView from django_filters.rest_framework import DjangoFilterBackend +from documents.filters import ObjectOwnedOrGrandtedPermissionsFilter from documents.permissions import PaperlessAdminPermissions -from documents.permissions import PaperlessModelPermissions +from documents.permissions import PaperlessObjectPermissions from documents.tasks import consume_file from packaging import version as packaging_version from paperless import version @@ -146,8 +147,12 @@ class CorrespondentViewSet(ModelViewSet): serializer_class = CorrespondentSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) - filter_backends = (DjangoFilterBackend, OrderingFilter) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) + filter_backends = ( + DjangoFilterBackend, + OrderingFilter, + ObjectOwnedOrGrandtedPermissionsFilter, + ) filterset_class = CorrespondentFilterSet ordering_fields = ( "name", @@ -172,7 +177,7 @@ class TagViewSet(ModelViewSet): return TagSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) filter_backends = (DjangoFilterBackend, OrderingFilter) filterset_class = TagFilterSet ordering_fields = ("name", "matching_algorithm", "match", "document_count") @@ -187,7 +192,7 @@ class DocumentTypeViewSet(ModelViewSet): serializer_class = DocumentTypeSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) filter_backends = (DjangoFilterBackend, OrderingFilter) filterset_class = DocumentTypeFilterSet ordering_fields = ("name", "matching_algorithm", "match", "document_count") @@ -204,7 +209,7 @@ class DocumentViewSet( queryset = Document.objects.all() serializer_class = DocumentSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filterset_class = DocumentFilterSet search_fields = ("title", "correspondent__name", "content") @@ -552,7 +557,7 @@ class SavedViewViewSet(ModelViewSet): queryset = SavedView.objects.all() serializer_class = SavedViewSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) def get_queryset(self): user = self.request.user @@ -828,7 +833,7 @@ class StoragePathViewSet(ModelViewSet): serializer_class = StoragePathSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) filter_backends = (DjangoFilterBackend, OrderingFilter) filterset_class = StoragePathFilterSet ordering_fields = ("name", "path", "matching_algorithm", "match", "document_count") diff --git a/src/paperless/settings.py b/src/paperless/settings.py index dd078f0f9..966f8cc3f 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -258,6 +258,11 @@ CHANNEL_LAYERS = { # Security # ############################################################################### +AUTHENTICATION_BACKENDS = [ + "guardian.backends.ObjectPermissionBackend", + "django.contrib.auth.backends.ModelBackend", +] + AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME") if AUTO_LOGIN_USERNAME: @@ -274,11 +279,7 @@ HTTP_REMOTE_USER_HEADER_NAME = os.getenv( if ENABLE_HTTP_REMOTE_USER: MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware") - AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.RemoteUserBackend", - "django.contrib.auth.backends.ModelBackend", - "guardian.backends.ObjectPermissionBackend", - ] + AUTHENTICATION_BACKENDS.insert(0, "django.contrib.auth.backends.RemoteUserBackend") REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append( "rest_framework.authentication.RemoteUserAuthentication", ) diff --git a/src/paperless/views.py b/src/paperless/views.py index 431bbfd81..7ff1462d3 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -6,7 +6,7 @@ from django.db.models.functions import Lower from django.http import HttpResponse from django.views.generic import View from django_filters.rest_framework import DjangoFilterBackend -from documents.permissions import PaperlessModelPermissions +from documents.permissions import PaperlessObjectPermissions from paperless.filters import GroupFilterSet from paperless.filters import UserFilterSet from paperless.serialisers import GroupSerializer @@ -43,7 +43,7 @@ class UserViewSet(ModelViewSet): serializer_class = UserSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) filter_backends = (DjangoFilterBackend, OrderingFilter) filterset_class = UserFilterSet ordering_fields = ("username",) @@ -56,7 +56,7 @@ class GroupViewSet(ModelViewSet): serializer_class = GroupSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated, PaperlessModelPermissions) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) filter_backends = (DjangoFilterBackend, OrderingFilter) filterset_class = GroupFilterSet ordering_fields = ("name",)