Skip to content

Commit ea1145c

Browse files
committed
Merge pull request #2905 from ticosax/django-object-perm-get_queryset
Allow DjangoObjectPermissions to use views that define get_queryset
2 parents 21cb808 + 031ac2a commit ea1145c

File tree

3 files changed

+55
-12
lines changed

3 files changed

+55
-12
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: 18 additions & 11 deletions
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

Lines changed: 36 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy