Skip to content

Commit 7cd5914

Browse files
author
Carlton Gibson
authored
Merge pull request #5376 from rpkilby/django-perms-queryset
DjangoModelPermissions should perform auth check before accessing the view's queryset
2 parents 139c8fe + 23b2d80 commit 7cd5914

File tree

3 files changed

+62
-32
lines changed

3 files changed

+62
-32
lines changed

docs/topics/release-notes.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`:
4040

4141
## 3.6.x series
4242

43+
### 3.6.5
44+
45+
* Fix `DjangoModelPermissions` to ensure user authentication before calling the view's `get_queryset()` method. As a side effect, this changes the order of the HTTP method permissions and authentication checks, and 405 responses will only be returned when authenticated. If you want to replicate the old behavior, see the PR for details. [#5376][gh5376]
46+
4347
### 3.6.4
4448

4549
**Date**: [21st August 2017][3.6.4-milestone]
@@ -1417,5 +1421,5 @@ For older release notes, [please see the version 2.x documentation][old-release-
14171421
[gh5147]: https://github.com/encode/django-rest-framework/issues/5147
14181422
[gh5131]: https://github.com/encode/django-rest-framework/issues/5131
14191423

1420-
1421-
1424+
<!-- 3.6.5 -->
1425+
[gh5376]: https://github.com/encode/django-rest-framework/issues/5376

rest_framework/permissions.py

+21-26
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,35 @@ def get_required_permissions(self, method, model_cls):
114114

115115
return [perm % kwargs for perm in self.perms_map[method]]
116116

117-
def has_permission(self, request, view):
118-
# Workaround to ensure DjangoModelPermissions are not applied
119-
# to the root view when using DefaultRouter.
120-
if getattr(view, '_ignore_model_permissions', False):
121-
return True
117+
def _queryset(self, view):
118+
assert hasattr(view, 'get_queryset') \
119+
or getattr(view, 'queryset', None) is not None, (
120+
'Cannot apply {} on a view that does not set '
121+
'`.queryset` or have a `.get_queryset()` method.'
122+
).format(self.__class__.__name__)
122123

123124
if hasattr(view, 'get_queryset'):
124125
queryset = view.get_queryset()
125126
assert queryset is not None, (
126127
'{}.get_queryset() returned None'.format(view.__class__.__name__)
127128
)
128-
else:
129-
queryset = getattr(view, 'queryset', None)
129+
return queryset
130+
return view.queryset
130131

131-
assert queryset is not None, (
132-
'Cannot apply DjangoModelPermissions on a view that '
133-
'does not set `.queryset` or have a `.get_queryset()` method.'
134-
)
132+
def has_permission(self, request, view):
133+
# Workaround to ensure DjangoModelPermissions are not applied
134+
# to the root view when using DefaultRouter.
135+
if getattr(view, '_ignore_model_permissions', False):
136+
return True
135137

138+
if not request.user or (
139+
not is_authenticated(request.user) and self.authenticated_users_only):
140+
return False
141+
142+
queryset = self._queryset(view)
136143
perms = self.get_required_permissions(request.method, queryset.model)
137144

138-
return (
139-
request.user and
140-
(is_authenticated(request.user) or not self.authenticated_users_only) and
141-
request.user.has_perms(perms)
142-
)
145+
return request.user.has_perms(perms)
143146

144147

145148
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
@@ -183,16 +186,8 @@ def get_required_object_permissions(self, method, model_cls):
183186
return [perm % kwargs for perm in self.perms_map[method]]
184187

185188
def has_object_permission(self, request, view, obj):
186-
if hasattr(view, 'get_queryset'):
187-
queryset = view.get_queryset()
188-
else:
189-
queryset = getattr(view, 'queryset', None)
190-
191-
assert queryset is not None, (
192-
'Cannot apply DjangoObjectPermissions on a view that '
193-
'does not set `.queryset` or have a `.get_queryset()` method.'
194-
)
195-
189+
# authentication checks have already executed via has_permission
190+
queryset = self._queryset(view)
196191
model_cls = queryset.model
197192
user = request.user
198193

tests/test_permissions.py

+35-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from rest_framework import (
1111
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
12-
status
12+
status, views
1313
)
1414
from rest_framework.compat import ResolverMatch, guardian, set_many
1515
from rest_framework.filters import DjangoObjectPermissionsFilter
@@ -201,14 +201,45 @@ def test_empty_view_does_not_assert(self):
201201
self.assertEqual(response.status_code, status.HTTP_200_OK)
202202

203203
def test_calling_method_not_allowed(self):
204-
request = factory.generic('METHOD_NOT_ALLOWED', '/')
204+
request = factory.generic('METHOD_NOT_ALLOWED', '/', HTTP_AUTHORIZATION=self.permitted_credentials)
205205
response = root_view(request)
206206
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
207207

208-
request = factory.generic('METHOD_NOT_ALLOWED', '/1')
208+
request = factory.generic('METHOD_NOT_ALLOWED', '/1', HTTP_AUTHORIZATION=self.permitted_credentials)
209209
response = instance_view(request, pk='1')
210210
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
211211

212+
def test_check_auth_before_queryset_call(self):
213+
class View(RootView):
214+
def get_queryset(_):
215+
self.fail('should not reach due to auth check')
216+
view = View.as_view()
217+
218+
request = factory.get('/', HTTP_AUTHORIZATION='')
219+
response = view(request)
220+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
221+
222+
def test_queryset_assertions(self):
223+
class View(views.APIView):
224+
authentication_classes = [authentication.BasicAuthentication]
225+
permission_classes = [permissions.DjangoModelPermissions]
226+
view = View.as_view()
227+
228+
request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials)
229+
msg = 'Cannot apply DjangoModelPermissions on a view that does not set `.queryset` or have a `.get_queryset()` method.'
230+
with self.assertRaisesMessage(AssertionError, msg):
231+
view(request)
232+
233+
# Faulty `get_queryset()` methods should trigger the above "view does not have a queryset" assertion.
234+
class View(RootView):
235+
def get_queryset(self):
236+
return None
237+
view = View.as_view()
238+
239+
request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials)
240+
with self.assertRaisesMessage(AssertionError, 'View.get_queryset() returned None'):
241+
view(request)
242+
212243

213244
class BasicPermModel(models.Model):
214245
text = models.CharField(max_length=100)
@@ -396,7 +427,7 @@ def test_cannot_read_list_permissions(self):
396427
self.assertListEqual(response.data, [])
397428

398429
def test_cannot_method_not_allowed(self):
399-
request = factory.generic('METHOD_NOT_ALLOWED', '/')
430+
request = factory.generic('METHOD_NOT_ALLOWED', '/', HTTP_AUTHORIZATION=self.credentials['readonly'])
400431
response = object_permissions_list_view(request)
401432
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
402433

0 commit comments

Comments
 (0)