Skip to content

Commit 63c84c3

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

File tree

3 files changed

+67
-11
lines changed

3 files changed

+67
-11
lines changed

docs/api-guide/permissions.md

Lines changed: 1 addition & 1 deletion
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

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@
88
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
99

1010

11+
def get_queryset_from_view(view):
12+
"""
13+
Do what it can to return the queryset or None.
14+
It is up to the caller (Permission) to handle
15+
None values. Some permissions backend allows
16+
such behavior.
17+
"""
18+
getter = getattr(view, 'get_queryset', None)
19+
if getter is not None:
20+
try:
21+
return view.get_queryset()
22+
except AssertionError:
23+
# The default implementation of view.get_queryset()
24+
# didn't find the view.queryset attribute.
25+
return
26+
else:
27+
try:
28+
return view.queryset
29+
except AttributeError:
30+
return
31+
32+
1133
class BasePermission(object):
1234
"""
1335
A base class from which all permission classes should inherit.
@@ -107,13 +129,7 @@ def get_required_permissions(self, method, model_cls):
107129
return [perm % kwargs for perm in self.perms_map[method]]
108130

109131
def has_permission(self, request, view):
110-
try:
111-
queryset = view.get_queryset()
112-
except AttributeError:
113-
queryset = getattr(view, 'queryset', None)
114-
except AssertionError:
115-
# view.get_queryset() didn't find .queryset
116-
queryset = None
132+
queryset = get_queryset_from_view(view)
117133

118134
# Workaround to ensure DjangoModelPermissions are not applied
119135
# to the root view when using DefaultRouter.
@@ -122,8 +138,8 @@ def has_permission(self, request, view):
122138

123139
assert queryset is not None, (
124140
'Cannot apply DjangoModelPermissions on a view that '
125-
'does not have `.queryset` property nor redefines `.get_queryset()`.'
126-
)
141+
'does not have `.queryset` property or override the '
142+
'`.get_queryset()` method.')
127143

128144
perms = self.get_required_permissions(request.method, queryset.model)
129145

@@ -172,7 +188,13 @@ def get_required_object_permissions(self, method, model_cls):
172188
return [perm % kwargs for perm in self.perms_map[method]]
173189

174190
def has_object_permission(self, request, view, obj):
175-
model_cls = view.queryset.model
191+
queryset = get_queryset_from_view(view)
192+
193+
assert queryset is not None, (
194+
'Cannot apply DjangoObjectPermissions on a view that '
195+
'does not have `.queryset` property or override the '
196+
'`.get_queryset()` method.')
197+
model_cls = queryset.model
176198
user = request.user
177199

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

tests/test_permissions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
77
from rest_framework.compat import guardian, get_model_name
88
from rest_framework.filters import DjangoObjectPermissionsFilter
9+
from rest_framework.routers import DefaultRouter
910
from rest_framework.test import APIRequestFactory
1011
from tests.models import BasicModel
1112
import base64
@@ -49,6 +50,7 @@ class EmptyListView(generics.ListCreateAPIView):
4950

5051

5152
root_view = RootView.as_view()
53+
api_root_view = DefaultRouter().get_api_root_view()
5254
instance_view = InstanceView.as_view()
5355
get_queryset_list_view = GetQuerySetListView.as_view()
5456
empty_list_view = EmptyListView.as_view()
@@ -86,6 +88,17 @@ def test_has_create_permissions(self):
8688
response = root_view(request, pk=1)
8789
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
8890

91+
def test_api_root_view_has_create_permissions(self):
92+
"""
93+
We check that DEFAULT_PERMISSION_CLASSES can
94+
apply to APIRoot view. More specifically we check expected behavior of
95+
``_ignore_model_permissions`` attribute support.
96+
"""
97+
request = factory.post('/', {'text': 'foobar'}, format='json',
98+
HTTP_AUTHORIZATION=self.permitted_credentials)
99+
response = api_root_view(request, pk=1)
100+
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
101+
89102
def test_get_queryset_has_create_permissions(self):
90103
request = factory.post('/', {'text': 'foobar'}, format='json',
91104
HTTP_AUTHORIZATION=self.permitted_credentials)
@@ -227,6 +240,18 @@ class ObjectPermissionListView(generics.ListAPIView):
227240
object_permissions_list_view = ObjectPermissionListView.as_view()
228241

229242

243+
class GetQuerysetObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
244+
serializer_class = BasicPermSerializer
245+
authentication_classes = [authentication.BasicAuthentication]
246+
permission_classes = [ViewObjectPermissions]
247+
248+
def get_queryset(self):
249+
return BasicPermModel.objects.all()
250+
251+
252+
get_queryset_object_permissions_view = GetQuerysetObjectPermissionInstanceView.as_view()
253+
254+
230255
@unittest.skipUnless(guardian, 'django-guardian not installed')
231256
class ObjectPermissionsIntegrationTests(TestCase):
232257
"""
@@ -326,6 +351,15 @@ def test_cannot_read_permissions(self):
326351
response = object_permissions_view(request, pk='1')
327352
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
328353

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

0 commit comments

Comments
 (0)