diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 8731cab08d..5748813784 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -150,7 +150,7 @@ Similar to `DjangoModelPermissions`, but also allows unauthenticated users to ha This permission class ties into Django's standard [object permissions framework][objectpermissions] that allows per-object permissions on models. In order to use this permission class, you'll also need to add a permission backend that supports object-level permissions, such as [django-guardian][guardian]. -As with `DjangoModelPermissions`, this permission must only be applied to views that have a `.queryset` property. Authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned. +As with `DjangoModelPermissions`, this permission must only be applied to views that have a `.queryset` property or `.get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned. * `POST` requests require the user to have the `add` permission on the model instance. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance. diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 8fa7e44523..0089966a1e 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,6 +8,28 @@ SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') +def get_queryset_from_view(view): + """ + Do what it can to return the queryset or None. + It is up to the caller (Permission) to handle + None values. Some permissions backend allows + such behavior. + """ + getter = getattr(view, 'get_queryset', None) + if getter is not None: + try: + return view.get_queryset() + except AssertionError: + # The default implementation of view.get_queryset() + # didn't find the view.queryset attribute. + return + else: + try: + return view.queryset + except AttributeError: + return + + class BasePermission(object): """ A base class from which all permission classes should inherit. @@ -107,13 +129,7 @@ def get_required_permissions(self, method, model_cls): return [perm % kwargs for perm in self.perms_map[method]] def has_permission(self, request, view): - try: - queryset = view.get_queryset() - except AttributeError: - queryset = getattr(view, 'queryset', None) - except AssertionError: - # view.get_queryset() didn't find .queryset - queryset = None + queryset = get_queryset_from_view(view) # Workaround to ensure DjangoModelPermissions are not applied # to the root view when using DefaultRouter. @@ -122,8 +138,8 @@ def has_permission(self, request, view): assert queryset is not None, ( 'Cannot apply DjangoModelPermissions on a view that ' - 'does not have `.queryset` property nor redefines `.get_queryset()`.' - ) + 'does not have `.queryset` property or override the ' + '`.get_queryset()` method.') perms = self.get_required_permissions(request.method, queryset.model) @@ -172,7 +188,13 @@ def get_required_object_permissions(self, method, model_cls): return [perm % kwargs for perm in self.perms_map[method]] def has_object_permission(self, request, view, obj): - model_cls = view.queryset.model + queryset = get_queryset_from_view(view) + + assert queryset is not None, ( + 'Cannot apply DjangoObjectPermissions on a view that ' + 'does not have `.queryset` property or override the ' + '`.get_queryset()` method.') + model_cls = queryset.model user = request.user perms = self.get_required_object_permissions(request.method, model_cls) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 9225308c7f..6e7bd41185 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -6,6 +6,7 @@ from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework.compat import guardian, get_model_name from rest_framework.filters import DjangoObjectPermissionsFilter +from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory from tests.models import BasicModel import base64 @@ -49,6 +50,7 @@ class EmptyListView(generics.ListCreateAPIView): root_view = RootView.as_view() +api_root_view = DefaultRouter().get_api_root_view() instance_view = InstanceView.as_view() get_queryset_list_view = GetQuerySetListView.as_view() empty_list_view = EmptyListView.as_view() @@ -86,6 +88,17 @@ def test_has_create_permissions(self): response = root_view(request, pk=1) self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_api_root_view_has_create_permissions(self): + """ + We check that DEFAULT_PERMISSION_CLASSES can + apply to APIRoot view. More specifically we check expected behavior of + ``_ignore_model_permissions`` attribute support. + """ + request = factory.post('/', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.permitted_credentials) + response = api_root_view(request, pk=1) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + def test_get_queryset_has_create_permissions(self): request = factory.post('/', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.permitted_credentials) @@ -227,6 +240,18 @@ class ObjectPermissionListView(generics.ListAPIView): object_permissions_list_view = ObjectPermissionListView.as_view() +class GetQuerysetObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): + serializer_class = BasicPermSerializer + authentication_classes = [authentication.BasicAuthentication] + permission_classes = [ViewObjectPermissions] + + def get_queryset(self): + return BasicPermModel.objects.all() + + +get_queryset_object_permissions_view = GetQuerysetObjectPermissionInstanceView.as_view() + + @unittest.skipUnless(guardian, 'django-guardian not installed') class ObjectPermissionsIntegrationTests(TestCase): """ @@ -326,6 +351,15 @@ def test_cannot_read_permissions(self): response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_can_read_get_queryset_permissions(self): + """ + same as ``test_can_read_permissions`` but with a view + that rely on ``.get_queryset()`` instead of ``.queryset``. + """ + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + response = get_queryset_object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + # Read list def test_can_read_list_permissions(self): request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])