8000 first pass at object level permissions and tests · lalkaka/django-rest-framework@118645e · GitHub
[go: up one dir, main page]

Skip to content

Commit 118645e

Browse files
committed
first pass at object level permissions and tests
1 parent 57d6b5f commit 118645e

File tree

2 files changed

+156
-36
lines changed

2 files changed

+156
-36
lines changed

rest_framework/permissions.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
99

10+
from django.http import Http404
1011
from rest_framework.compat import oauth2_provider_scope, oauth2_constants, guardian
1112

1213

@@ -152,9 +153,54 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
152153

153154

154155
class DjangoObjectLevelModelPermissions(DjangoModelPermissions):
156+
"""
157+
Basic object level permissions utilizing django-guardian.
158+
"""
159+
155160
def __init__(self):
156161
assert guardian, 'Using DjangoObjectLevelModelPermissions, but django-guardian is not installed'
157162

163+
action_perm_map = {
164+
'list': 'read',
165+
'retrieve': 'read',
166+
'create': 'add',
167+
'partial_update': 'change',
168+
'update': 'change',
169+
'destroy': 'delete',
170+
}
171+
172+
def _get_names(self, view):
173+
model_cls = getattr(view, 'model', None)
174+
queryset = getattr(view, 'queryset', None)
175+
176+
if model_cls is None and queryset is not None:
177+
model_cls = queryset.model
178+
if not model_cls: # no model, no model based permissions
179+
return None
180+
model_name = model_cls._meta.module_name
181+
return model_name
182+
183+
def has_permission(self, request, view):
184+
if view.action == 'list':
185+
user = request.user
186+
queryset = view.get_queryset()
187+
model_name = self._get_names(view)
188+
view.queryset = guardian.shortcuts.get_objects_for_user(user, 'read_' + model_name, queryset) #TODO: move to filter
189+
return super(DjangoObjectLevelModelPermissions, self).has_permission(request, view)
190+
191+
def has_object_permission(self, request, view, obj):
192+
user = request.user
193+
model_name = self._get_names(view)
194+
action = self.action_perm_map.get(view.action)
195+
196+
assert action, "Tried to determine object permissions but no action specified in view"
197+
198+
perm = "{action}_{model_name}".format(action=action, model_name=model_name)
199+
check = user.has_perm(perm, obj)
200+
if not check:
201+
raise Http404
202+
return user.has_perm(perm, obj)
203+
158204

159205
class TokenHasReadWriteScope(BasePermission):
160206
"""

rest_framework/tests/test_permissions.py

Lines changed: 110 additions & 36 deletions
F438
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from __future__ import unicode_literals
2-
from django.contrib.auth.models import User, Permission
2+
from django.contrib.auth.models import User, Permission, Group
33
from django.db import models
44
from django.test import TestCase
55
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
66
from rest_framework.compat import guardian
77
from rest_framework.test import APIRequestFactory
88
from rest_framework.tests.models import BasicModel
9-
from rest_framework.settings import api_settings
109
import base64
1110

1211
factory = APIRequestFactory()
@@ -142,67 +141,142 @@ def test_options_updateonly(self):
142141
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
143142

144143

145-
class BasicPermModel(BasicModel):
144+
class BasicPermModel(models.Model):
145+
text = models.CharField(max_length=100)
146146

147147
class Meta:
148148
app_label = 'tests'
149149
permissions = (
150-
('read_basicpermmodel', "Can view basic perm model"),
150+
('read_basicpermmodel', 'Can view basic perm model'),
151151
# add, change, delete built in to django
152152
)
153153

154154
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
155-
model = BasicModel
155+
model = BasicPermModel
156156
authentication_classes = [authentication.BasicAuthentication]
157157
permission_classes = [permissions.DjangoObjectLevelModelPermissions]
158158

159-
160159
object_permissions_view = ObjectPermissionInstanceView.as_view()
161160

161+
class ObjectPermissionListView(generics.ListAPIView):
162+
model = BasicPermModel
163+
authentication_classes = [authentication.BasicAuthentication]
164+
permission_classes = [permissions.DjangoObjectLevelModelPermissions]
165+
166+
object_permissions_list_view = ObjectPermissionListView.as_view()
167+
162168
if guardian:
169+
from guardian.shortcuts import assign_perm
170+
163171
class ObjectPermissionsIntegrationTests(TestCase):
164172
"""
165173
Integration tests for the object level permissions API.
166174
"""
175+
@classmethod
176+
def setUpClass(cls):
177+
# create users
178+
create = User.objects.create_user
179+
users = {
180+
'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'),
181+
'readonly': create('readonly', 'readonly@example.com', 'password'),
182+
'writeonly': create('writeonly', 'writeonly@example.com', 'password'),
183+
'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'),
184+
}
185+
186+
# give everyone model level permissions, as we are not testing those
187+
everyone = Group.objects.create(name='everyone')
188+
model_name = BasicPermModel._meta.module_name
189+
app_label = BasicPermModel._meta.app_label
190+
f = '{0}_{1}'.format
191+
perms = {
192+
'read': f('read', model_name),
193+
'change': f('change', model_name),
194+
'delete': f('delete', model_name)
195+
}
196+
for perm in perms.values():
197+
perm = '{0}.{1}'.format(app_label, perm)
198+
assign_perm(perm, everyone)
199+
everyone.user_set.add(*users.values())
200+
201+
cls.perms = perms
202+
cls.users = users
167203

168204
def setUp(self):
169-
# create users
170-
User.objects.create_user('no_permission', 'no_permission@example.com', 'password')
171-
reader = User.objects.create_user('reader', 'reader@example.com', 'password')
172-
writer = User.objects.create_user('writer', 'writer@example.com', 'password')
173-
full_access = User.objects.create_user('full_access', 'full_access@example.com', 'password')
174-
175-
model = BasicPermModel.objects.create(text='foo')
205+
perms = self.perms
206+
users = self.users
176207

177-
# assign permissions appropriately
178-
from guardian.shortcuts import assign_perm
208+
# appropriate object level permissions
209+
readers = Group.objects.create(name='readers')
210+
writers = Group.objects.create(name='writers')
211+
deleters = Group.objects.create(name='deleters')
179212

180-
read = "read_basicpermmodel"
181-
write = "change_basicpermmodel"
182-
delete = "delete_basicpermmodel"
183-
app_label = 'tests.'
184-
# model level permissions
185-
assign_perm(app_label + delete, full_access, obj=model)
186-
(assign_perm(app_label + write, user, obj=model) for user in (writer, full_access))
187-
(assign_perm(app_label + read, user, obj=model) for user in (reader, writer, full_access))
213+
model = BasicPermModel.objects.create(text='foo')
214+
215+
assign_perm(perms['read'], readers, model)
216+
assign_perm(perms['change'], writers, model)
217+
assign_perm(perms['delete'], deleters, model)
218+
219+
readers.user_set.add(users['fullaccess'], users['readonly'])
220+
writers.user_set.add(users['fullaccess'], users['writeonly'])
221+
deleters.user_set.add(users['fullaccess'], users['deleteonly'])
222+
223+
self.credentials = {}
224+
for user in users.values():
225+
self.credentials[user.username] = basic_auth_header(user.username, 'password')
226+
227+
# Delete
228+
def test_can_delete_permissions(self):
229+
request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly'])
230+
object_permissions_view.cls.action = 'destroy'
231+
response = object_permissions_view(request, pk='1')
232+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
188233

189-
# object level permissions
190-
assign_perm(delete, full_access, obj=model)
191-
(assign_perm(write, user, obj=model) for user in (writer, full_access))
192-
(assign_perm(read, user, obj=model) for user in (reader, writer, full_access))
234+
def test_cannot_delete_permissions(self):
235+
request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
236+
object_permissions_view.cls.action = 'destroy'
237+
response = object_permissions_view(request, pk='1')
238+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
193239

194-
self.no_permission_credentials = basic_auth_header('no_permission', 'password')
195-
self.reader_credentials = basic_auth_header('reader', 'password')
196-
self.writer_credentials = basic_auth_header('writer', 'password')
197-
self.full_access_credentials = basic_auth_header('full_access', 'password')
240+
# Update
241+
def test_can_update_permissions(self):
242+
request = factory.patch('/1', {'text': 'foobar'}, format='json',
243+
HTTP_AUTHORIZATION=self.credentials['writeonly'])
244+
object_permissions_view.cls.action = 'partial_update'
245+
response = object_permissions_view(request, pk='1')
246+
self.assertEqual(response.status_code, status.HTTP_200_OK)
247+
self.assertEqual(response.data.get('text'), 'foobar')
198248

249+
def test_cannot_update_permissions(self):
250+
request = factory.patch('/1', {'text': 'foobar'}, format='json',
251+
HTTP_AUTHORIZATION=self.credentials['deleteonly'])
252+
object_permissions_view.cls.action = 'partial_update'
253+
response = object_permissions_view(request, pk='1')
254+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
199255

200-
def test_has_delete_permissions(self):
201-
request = factory.delete('/1', HTTP_AUTHORIZATION=self.full_access_credentials)
256+
# Read
257+
def test_can_read_permissions(self):
258+
request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
259+
object_permissions_view.cls.action = 'retrieve'
202260
response = object_permissions_view(request, pk='1')
203-
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
261+
self.assertEqual(response.status_code, status.HTTP_200_OK)
204262

205-
def test_no_delete_permissions(self):
206-
request = factory.delete('/1', HTTP_AUTHORIZATION=self.writer_credentials)
263+
def test_cannot_read_permissions(self):
264+
request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly'])
265+
object_permissions_view.cls.action = 'retrieve'
207266
response = object_permissions_view(request, pk='1')
208267
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
268+
269+
# Read list
270+
def test_can_read_list_permissions(self):
271+
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])
272+
object_permissions_list_view.cls.action = 'list'
273+
response = object_permissions_list_view(request)
274+
self.assertEqual(response.status_code, status.HTTP_200_OK)
275+
self.assertEqual(response.data[0].get('id'), 1)
276+
277+
def test_cannot_read_list_permissions(self):
278+
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly'])
279+
object_permissions_list_view.cls.action = 'list'
280+
response = object_permissions_list_view(request)
281+
self.assertEqual(response.status_code, status.HTTP_200_OK)
282+
self.assertListEqual(response.data, [])

0 commit comments

Comments
 (0)
0