Object-level permissions + filtering

This commit is contained in:
Michael Shamoon 2022-12-05 22:56:03 -08:00
parent dbaa606a9f
commit fad13c148e
5 changed files with 52 additions and 20 deletions

View File

@ -2,6 +2,7 @@ from django.db.models import Q
from django_filters.rest_framework import BooleanFilter from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import Filter from django_filters.rest_framework import Filter
from django_filters.rest_framework import FilterSet from django_filters.rest_framework import FilterSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from .models import Correspondent from .models import Correspondent
from .models import Document from .models import Document
@ -134,3 +135,17 @@ class StoragePathFilterSet(FilterSet):
"name": CHAR_KWARGS, "name": CHAR_KWARGS,
"path": 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

View File

@ -1,18 +1,29 @@
from rest_framework.permissions import BasePermission 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 = { perms_map = {
"GET": ["%(app_label)s.view_%(model_name)s"], "GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": [], "OPTIONS": ["%(app_label)s.view_%(model_name)s"],
"HEAD": [], "HEAD": ["%(app_label)s.view_%(model_name)s"],
"POST": ["%(app_label)s.add_%(model_name)s"], "POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"], "PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(model_name)s"], "PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(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): class PaperlessAdminPermissions(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):

View File

@ -28,8 +28,9 @@ from django.utils.translation import get_language
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from documents.filters import ObjectOwnedOrGrandtedPermissionsFilter
from documents.permissions import PaperlessAdminPermissions from documents.permissions import PaperlessAdminPermissions
from documents.permissions import PaperlessModelPermissions from documents.permissions import PaperlessObjectPermissions
from documents.tasks import consume_file from documents.tasks import consume_file
from packaging import version as packaging_version from packaging import version as packaging_version
from paperless import version from paperless import version
@ -146,8 +147,12 @@ class CorrespondentViewSet(ModelViewSet):
serializer_class = CorrespondentSerializer serializer_class = CorrespondentSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (
DjangoFilterBackend,
OrderingFilter,
ObjectOwnedOrGrandtedPermissionsFilter,
)
filterset_class = CorrespondentFilterSet filterset_class = CorrespondentFilterSet
ordering_fields = ( ordering_fields = (
"name", "name",
@ -172,7 +177,7 @@ class TagViewSet(ModelViewSet):
return TagSerializer return TagSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = TagFilterSet filterset_class = TagFilterSet
ordering_fields = ("name", "matching_algorithm", "match", "document_count") ordering_fields = ("name", "matching_algorithm", "match", "document_count")
@ -187,7 +192,7 @@ class DocumentTypeViewSet(ModelViewSet):
serializer_class = DocumentTypeSerializer serializer_class = DocumentTypeSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = DocumentTypeFilterSet filterset_class = DocumentTypeFilterSet
ordering_fields = ("name", "matching_algorithm", "match", "document_count") ordering_fields = ("name", "matching_algorithm", "match", "document_count")
@ -204,7 +209,7 @@ class DocumentViewSet(
queryset = Document.objects.all() queryset = Document.objects.all()
serializer_class = DocumentSerializer serializer_class = DocumentSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filterset_class = DocumentFilterSet filterset_class = DocumentFilterSet
search_fields = ("title", "correspondent__name", "content") search_fields = ("title", "correspondent__name", "content")
@ -552,7 +557,7 @@ class SavedViewViewSet(ModelViewSet):
queryset = SavedView.objects.all() queryset = SavedView.objects.all()
serializer_class = SavedViewSerializer serializer_class = SavedViewSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
@ -828,7 +833,7 @@ class StoragePathViewSet(ModelViewSet):
serializer_class = StoragePathSerializer serializer_class = StoragePathSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = StoragePathFilterSet filterset_class = StoragePathFilterSet
ordering_fields = ("name", "path", "matching_algorithm", "match", "document_count") ordering_fields = ("name", "path", "matching_algorithm", "match", "document_count")

View File

@ -258,6 +258,11 @@ CHANNEL_LAYERS = {
# Security # # Security #
############################################################################### ###############################################################################
AUTHENTICATION_BACKENDS = [
"guardian.backends.ObjectPermissionBackend",
"django.contrib.auth.backends.ModelBackend",
]
AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME") AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME")
if AUTO_LOGIN_USERNAME: if AUTO_LOGIN_USERNAME:
@ -274,11 +279,7 @@ HTTP_REMOTE_USER_HEADER_NAME = os.getenv(
if ENABLE_HTTP_REMOTE_USER: if ENABLE_HTTP_REMOTE_USER:
MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware") MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS.insert(0, "django.contrib.auth.backends.RemoteUserBackend")
"django.contrib.auth.backends.RemoteUserBackend",
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
]
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append( REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
"rest_framework.authentication.RemoteUserAuthentication", "rest_framework.authentication.RemoteUserAuthentication",
) )

View File

@ -6,7 +6,7 @@ from django.db.models.functions import Lower
from django.http import HttpResponse from django.http import HttpResponse
from django.views.generic import View from django.views.generic import View
from django_filters.rest_framework import DjangoFilterBackend 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 GroupFilterSet
from paperless.filters import UserFilterSet from paperless.filters import UserFilterSet
from paperless.serialisers import GroupSerializer from paperless.serialisers import GroupSerializer
@ -43,7 +43,7 @@ class UserViewSet(ModelViewSet):
serializer_class = UserSerializer serializer_class = UserSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = UserFilterSet filterset_class = UserFilterSet
ordering_fields = ("username",) ordering_fields = ("username",)
@ -56,7 +56,7 @@ class GroupViewSet(ModelViewSet):
serializer_class = GroupSerializer serializer_class = GroupSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessModelPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = GroupFilterSet filterset_class = GroupFilterSet
ordering_fields = ("name",) ordering_fields = ("name",)