Skip to content

Commit eb2be3a

Browse files
committed
Allow DjangoObjectPermissions to use views that define get_queryset
1 parent 2b6726e commit eb2be3a

File tree

3 files changed

+56
-5
lines changed

3 files changed

+56
-5
lines changed

docs/api-guide/permissions.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Similar to `DjangoModelPermissions`, but also allows unauthenticated users to ha
150150

151151
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].
152152

153-
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.
153+
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.
154154

155155
* `POST` requests require the user to have the `add` permission on the model instance.
156156
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance.

rest_framework/permissions.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ def has_permission(self, request, view):
112112
except AttributeError:
113113
queryset = getattr(view, 'queryset', None)
114114
except AssertionError:
115-
# view.get_queryset() didn't find .queryset
115+
# The default implementation of view.get_queryset()
116+
# didn't find the view.queryset attribute.
116117
queryset = None
117118

118119
# Workaround to ensure DjangoModelPermissions are not applied
@@ -122,8 +123,8 @@ def has_permission(self, request, view):
122123

123124
assert queryset is not None, (
124125
'Cannot apply DjangoModelPermissions on a view that '
125-
'does not have `.queryset` property nor redefines `.get_queryset()`.'
126-
)
126+
'does not have `.queryset` property or overrides the '
127+
'`.get_queryset()` method.')
127128

128129
perms = self.get_required_permissions(request.method, queryset.model)
129130

@@ -172,7 +173,21 @@ def get_required_object_permissions(self, method, model_cls):
172173
return [perm % kwargs for perm in self.perms_map[method]]
173174

174175
def has_object_permission(self, request, view, obj):
175-
model_cls = view.queryset.model
176+
try:
177+
queryset = view.get_queryset()
178+
except AttributeError:
179+
queryset = getattr(view, 'queryset', None)
180+
except AssertionError:
181+
# The default implementation of view.get_queryset()
182+
# didn't find the view.queryset attribute.
183+
queryset = None
184+
185+
assert queryset is not None, (
186+
'Cannot apply DjangoObjectPermissions on a view that '
187+
'does not have `.queryset` property or overrides the '
188+
'`.get_queryset()` method.')
189+
190+
model_cls = queryset.model
176191
user = request.user
177192

178193
perms = self.get_required_object_permissions(request.method, model_cls)

tests/test_permissions.py

+36
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from django.utils import unittest
66
from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
77
from rest_framework.compat import guardian, get_model_name
8+
from django.core.urlresolvers import ResolverMatch
89
from rest_framework.filters import DjangoObjectPermissionsFilter
10+
from rest_framework.routers import DefaultRouter
911
from rest_framework.test import APIRequestFactory
1012
from tests.models import BasicModel
1113
import base64
@@ -49,6 +51,7 @@ class EmptyListView(generics.ListCreateAPIView):
4951

5052

5153
root_view = RootView.as_view()
54+
api_root_view = DefaultRouter().get_api_root_view()
5255
instance_view = InstanceView.as_view()
5356
get_queryset_list_view = GetQuerySetListView.as_view()
5457
empty_list_view = EmptyListView.as_view()
@@ -86,6 +89,18 @@ def test_has_create_permissions(self):
8689
response = root_view(request, pk=1)
8790
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
8891

92+
def test_api_root_view_discard_default_django_model_permission(self):
93+
"""
94+
We check that DEFAULT_PERMISSION_CLASSES can
95+
apply to APIRoot view. More specifically we check expected behavior of
96+
``_ignore_model_permissions`` attribute support.
97+
"""
98+
request = factory.get('/', format='json',
99+
HTTP_AUTHORIZATION=self.permitted_credentials)
100+
request.resolver_match = ResolverMatch('get', (), {})
101+
response = api_root_view(request)
102+
self.assertEqual(response.status_code, status.HTTP_200_OK)
103+
89104
def test_get_queryset_has_create_permissions(self):
90105
request = factory.post('/', {'text': 'foobar'}, format='json',
91106
HTTP_AUTHORIZATION=self.permitted_credentials)
@@ -227,6 +242,18 @@ class ObjectPermissionListView(generics.ListAPIView):
227242
object_permissions_list_view = ObjectPermissionListView.as_view()
228243

229244

245+
class GetQuerysetObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
246+
serializer_class = BasicPermSerializer
247+
authentication_classes = [authentication.BasicAuthentication]
248+
permission_classes = [ViewObjectPermissions]
249+
250+
def get_queryset(self):
251+
return BasicPermModel.objects.all()
252+
253+
254+
get_queryset_object_permissions_view = GetQuerysetObjectPermissionInstanceView.as_view()
255+
256+
230257
@unittest.skipUnless(guardian, 'django-guardian not installed')
231258
class ObjectPermissionsIntegrationTests(TestCase):
232259
"""
@@ -326,6 +353,15 @@ def test_cannot_read_permissions(self):
326353
response = object_permissions_view(request, pk='1')
327354
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
328355

356+
def test_can_read_get_queryset_permissions(self):
357+
"""
358+
same as ``test_can_read_permissions`` but with a view
359+
that rely on ``.get_queryset()`` instead of ``.queryset``.
360+
"""
361+
request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
362+
response = get_queryset_object_permissions_view(request, pk='1')
363+
self.assertEqual(response.status_code, status.HTTP_200_OK)
364+
329365
# Read list
330366
def test_can_read_list_permissions(self):
331367
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])

0 commit comments

Comments
 (0)