diff options
| author | Tom Christie | 2013-09-10 21:00:13 +0100 |
|---|---|---|
| committer | Tom Christie | 2013-09-10 21:00:13 +0100 |
| commit | 5970baa20112921217ae4f2c2a9f175df25922db (patch) | |
| tree | 40f4ae8e6d2ffd841c31b6a55f98d9823516d61c /rest_framework | |
| parent | 75fb4b02b40da04f16c6c288bbe20ea0bc0b4154 (diff) | |
| download | django-rest-framework-5970baa20112921217ae4f2c2a9f175df25922db.tar.bz2 | |
Tweaks and docs to object-level model permissions.
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/filters.py | 37 | ||||
| -rw-r--r-- | rest_framework/permissions.py | 47 | ||||
| -rw-r--r-- | rest_framework/tests/test_permissions.py | 249 |
3 files changed, 188 insertions, 145 deletions
diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 6d46ad23..1693bcd2 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -23,22 +23,6 @@ class BaseFilterBackend(object): raise NotImplementedError(".filter_queryset() must be overridden.") -class ObjectPermissionReaderFilter(BaseFilterBackend): - """ - A filter backend that limits results to those where the requesting user - has read object level permissions. - """ - def __init__(self): - assert guardian, 'Using ObjectPermissionReaderFilter, but django-guardian is not installed' - - def filter_queryset(self, request, queryset, view): - user = request.user - model_cls = queryset.model - model_name = model_cls._meta.module_name - permission = 'read_' + model_name - return guardian.shortcuts.get_objects_for_user(user, permission, queryset) - - class DjangoFilterBackend(BaseFilterBackend): """ A filter backend that uses django-filter. @@ -156,3 +140,24 @@ class OrderingFilter(BaseFilterBackend): return queryset.order_by(*ordering) return queryset + + +class DjangoObjectPermissionsFilter(BaseFilterBackend): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions. + """ + def __init__(self): + assert guardian, 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed' + + perm_format = '%(app_label)s.view_%(model_name)s' + + def filter_queryset(self, request, queryset, view): + user = request.user + model_cls = queryset.model + kwargs = { + 'app_label': model_cls._meta.app_label, + 'model_name': model_cls._meta.module_name + } + permission = self.perm_format % kwargs + return guardian.shortcuts.get_objects_for_user(user, permission, queryset) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 70bf9c61..53184798 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -152,10 +152,10 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): authenticated_users_only = False -class DjangoObjectLevelModelPermissions(DjangoModelPermissions): +class DjangoObjectPermissions(DjangoModelPermissions): """ - The request is authenticated using `django.contrib.auth` permissions. - See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions + The request is authenticated using Django's object-level permissions. + It requires an object-permissions-enabled backend, such as Django Guardian. It ensures that the user is authenticated, and has the appropriate `add`/`change`/`delete` permissions on the object using .has_perms. @@ -164,21 +164,22 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): provide a `.model` or `.queryset` attribute. """ - actions_map = { - 'GET': ['read_%(model_name)s'], - 'OPTIONS': ['read_%(model_name)s'], - 'HEAD': ['read_%(model_name)s'], - 'POST': ['add_%(model_name)s'], - 'PUT': ['change_%(model_name)s'], - 'PATCH': ['change_%(model_name)s'], - 'DELETE': ['delete_%(model_name)s'], + perms_map = { + 'GET': [], + 'OPTIONS': [], + 'HEAD': [], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], } def get_required_object_permissions(self, method, model_cls): kwargs = { + 'app_label': model_cls._meta.app_label, 'model_name': model_cls._meta.module_name } - return [perm % kwargs for perm in self.actions_map[method]] + return [perm % kwargs for perm in self.perms_map[method]] def has_object_permission(self, request, view, obj): model_cls = getattr(view, 'model', None) @@ -190,10 +191,24 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): perms = self.get_required_object_permissions(request.method, model_cls) user = request.user - check = user.has_perms(perms, obj) - if not check: - raise Http404 - return user.has_perms(perms, obj) + if not user.has_perms(perms, obj): + # If the user does not have permissions we need to determine if + # they have read permissions to see 403, or not, and simply see + # a 404 reponse. + + if request.method in ('GET', 'OPTIONS', 'HEAD'): + # Read permissions already checked and failed, no need + # to make another lookup. + raise Http404 + + read_perms = self.get_required_object_permissions('GET', model_cls) + if not user.has_perms(read_perms, obj): + raise Http404 + + # Has read permissions. + return False + + return True class TokenHasReadWriteScope(BasePermission): diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index 2d866cd0..d08124f4 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -2,9 +2,10 @@ from __future__ import unicode_literals from django.contrib.auth.models import User, Permission, Group from django.db import models from django.test import TestCase +from django.utils import unittest from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework.compat import guardian -from rest_framework.filters import ObjectPermissionReaderFilter +from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel import base64 @@ -148,130 +149,152 @@ class BasicPermModel(models.Model): class Meta: app_label = 'tests' permissions = ( - ('read_basicpermmodel', 'Can view basic perm model'), + ('view_basicpermmodel', 'Can view basic perm model'), # add, change, delete built in to django ) +# Custom object-level permission, that includes 'view' permissions +class ViewObjectPermissions(permissions.DjangoObjectPermissions): + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + + class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicPermModel authentication_classes = [authentication.BasicAuthentication] - permission_classes = [permissions.DjangoObjectLevelModelPermissions] + permission_classes = [ViewObjectPermissions] object_permissions_view = ObjectPermissionInstanceView.as_view() + class ObjectPermissionListView(generics.ListAPIView): model = BasicPermModel authentication_classes = [authentication.BasicAuthentication] - permission_classes = [permissions.DjangoObjectLevelModelPermissions] + permission_classes = [ViewObjectPermissions] object_permissions_list_view = ObjectPermissionListView.as_view() -if guardian: - from guardian.shortcuts import assign_perm - - class ObjectPermissionsIntegrationTests(TestCase): - """ - Integration tests for the object level permissions API. - """ - @classmethod - def setUpClass(cls): - # create users - create = User.objects.create_user - users = { - 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'), - 'readonly': create('readonly', 'readonly@example.com', 'password'), - 'writeonly': create('writeonly', 'writeonly@example.com', 'password'), - 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'), - } - - # give everyone model level permissions, as we are not testing those - everyone = Group.objects.create(name='everyone') - model_name = BasicPermModel._meta.module_name - app_label = BasicPermModel._meta.app_label - f = '{0}_{1}'.format - perms = { - 'read': f('read', model_name), - 'change': f('change', model_name), - 'delete': f('delete', model_name) - } - for perm in perms.values(): - perm = '{0}.{1}'.format(app_label, perm) - assign_perm(perm, everyone) - everyone.user_set.add(*users.values()) - - cls.perms = perms - cls.users = users - - def setUp(self): - perms = self.perms - users = self.users - - # appropriate object level permissions - readers = Group.objects.create(name='readers') - writers = Group.objects.create(name='writers') - deleters = Group.objects.create(name='deleters') - - model = BasicPermModel.objects.create(text='foo') - - assign_perm(perms['read'], readers, model) - assign_perm(perms['change'], writers, model) - assign_perm(perms['delete'], deleters, model) - - readers.user_set.add(users['fullaccess'], users['readonly']) - writers.user_set.add(users['fullaccess'], users['writeonly']) - deleters.user_set.add(users['fullaccess'], users['deleteonly']) - - self.credentials = {} - for user in users.values(): - self.credentials[user.username] = basic_auth_header(user.username, 'password') - - # Delete - def test_can_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_cannot_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - # Update - def test_can_update_permissions(self): - request = factory.patch('/1', {'text': 'foobar'}, format='json', - HTTP_AUTHORIZATION=self.credentials['writeonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('text'), 'foobar') - - def test_cannot_update_permissions(self): - request = factory.patch('/1', {'text': 'foobar'}, format='json', - HTTP_AUTHORIZATION=self.credentials['deleteonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - # Read - def test_can_read_permissions(self): - request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_cannot_read_permissions(self): - request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - # Read list - def test_can_read_list_permissions(self): - request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly']) - object_permissions_list_view.cls.filter_backends = (ObjectPermissionReaderFilter,) - response = object_permissions_list_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0].get('id'), 1) - - def test_cannot_read_list_permissions(self): - request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly']) - object_permissions_list_view.cls.filter_backends = (ObjectPermissionReaderFilter,) - response = object_permissions_list_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertListEqual(response.data, [])
\ No newline at end of file + +@unittest.skipUnless(guardian, 'django-guardian not installed') +class ObjectPermissionsIntegrationTests(TestCase): + """ + Integration tests for the object level permissions API. + """ + @classmethod + def setUpClass(cls): + from guardian.shortcuts import assign_perm + + # create users + create = User.objects.create_user + users = { + 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'), + 'readonly': create('readonly', 'readonly@example.com', 'password'), + 'writeonly': create('writeonly', 'writeonly@example.com', 'password'), + 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'), + } + + # give everyone model level permissions, as we are not testing those + everyone = Group.objects.create(name='everyone') + model_name = BasicPermModel._meta.module_name + app_label = BasicPermModel._meta.app_label + f = '{0}_{1}'.format + perms = { + 'view': f('view', model_name), + 'change': f('change', model_name), + 'delete': f('delete', model_name) + } + for perm in perms.values(): + perm = '{0}.{1}'.format(app_label, perm) + assign_perm(perm, everyone) + everyone.user_set.add(*users.values()) + + cls.perms = perms + cls.users = users + + def setUp(self): + from guardian.shortcuts import assign_perm + perms = self.perms + users = self.users + + # appropriate object level permissions + readers = Group.objects.create(name='readers') + writers = Group.objects.create(name='writers') + deleters = Group.objects.create(name='deleters') + + model = BasicPermModel.objects.create(text='foo') + + assign_perm(perms['view'], readers, model) + assign_perm(perms['change'], writers, model) + assign_perm(perms['delete'], deleters, model) + + readers.user_set.add(users['fullaccess'], users['readonly']) + writers.user_set.add(users['fullaccess'], users['writeonly']) + deleters.user_set.add(users['fullaccess'], users['deleteonly']) + + self.credentials = {} + for user in users.values(): + self.credentials[user.username] = basic_auth_header(user.username, 'password') + + # Delete + def test_can_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_cannot_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Update + def test_can_update_permissions(self): + request = factory.patch('/1', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['writeonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('text'), 'foobar') + + def test_cannot_update_permissions(self): + request = factory.patch('/1', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['deleteonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_cannot_update_permissions_non_existing(self): + request = factory.patch('/999', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['deleteonly']) + response = object_permissions_view(request, pk='999') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Read + def test_can_read_permissions(self): + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_cannot_read_permissions(self): + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Read list + def test_can_read_list_permissions(self): + request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly']) + object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,) + response = object_permissions_list_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0].get('id'), 1) + + def test_cannot_read_list_permissions(self): + request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly']) + object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,) + response = object_permissions_list_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual(response.data, []) |
