Skip to content

Commit 031ac2a

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

File tree

3 files changed

+55
-12
lines changed

3 files changed

+55
-12
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

+18-11
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,20 @@ def get_required_permissions(self, method, model_cls):
107107
return [perm % kwargs for perm in self.perms_map[method]]
108108

109109
def has_permission(self, request, view):
110+
# Workaround to ensure DjangoModelPermissions are not applied
111+
# to the root view when using DefaultRouter.
112+
if getattr(view, '_ignore_model_permissions', False):
113+
return True
114+
110115
try:
111116
queryset = view.get_queryset()
112117
except AttributeError:
113118
queryset = getattr(view, 'queryset', None)
114-
except AssertionError:
115-
# view.get_queryset() didn't find .queryset
116-
queryset = None
117-
118-
# Workaround to ensure DjangoModelPermissions are not applied
119-
# to the root view when using DefaultRouter.
120-
if queryset is None and getattr(view, '_ignore_model_permissions', False):
121-
return True
122119

123120
assert queryset is not None, (
124121
'Cannot apply DjangoModelPermissions on a view that '
125-
'does not have `.queryset` property nor redefines `.get_queryset()`.'
126-
)
122+
'does not have `.queryset` property or overrides the '
123+
'`.get_queryset()` method.')
127124

128125
perms = self.get_required_permissions(request.method, queryset.model)
129126

@@ -172,7 +169,17 @@ def get_required_object_permissions(self, method, model_cls):
172169
return [perm % kwargs for perm in self.perms_map[method]]
173170

174171
def has_object_permission(self, request, view, obj):
175-
model_cls = view.queryset.model
172+
try:
173+
queryset = view.get_queryset()
174+
except AttributeError:
175+
queryset = getattr(view, 'queryset', None)
176+
177+
assert queryset is not None, (
178+
'Cannot apply DjangoObjectPermissions on a view that '
179+
'does not have `.queryset` property or overrides the '
180+
'`.get_queryset()` method.')
181+
182+
model_cls = queryset.model
176183
user = request.user
177184

178185
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)