Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow permission classes to customize the failure message. #2539

Merged
merged 3 commits into from
Jun 24, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/api-guide/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ If you need to test if a request is a read operation or a write operation, you s

---

Custom permissions will raise a `PermissionDenied` exception if the test fails. To change the error message associated with the exception, implement a `message` attribute directly on your custom permission. Otherwise the `default_detail` attribute from `PermissionDenied` will be used.

from rest_framework import permissions

class CustomerAccessPermission(permissions.BasePermission):
message = 'Adding customers not allowed.'

def has_permission(self, request, view):
...

## Examples

The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted.
Expand Down
8 changes: 7 additions & 1 deletion rest_framework/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,14 @@ def http_method_not_allowed(self, request, *args, **kwargs):
"""
raise exceptions.MethodNotAllowed(request.method)

def permission_denied(self, request):
def permission_denied(self, request, message=None):
"""
If request is not permitted, determine what kind of exception to raise.
"""
if not request.successful_authenticator:
raise exceptions.NotAuthenticated()
if message is not None:
raise exceptions.PermissionDenied(message)
raise exceptions.PermissionDenied()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just simplify this to raise exceptions.PermissionDenied(message=message) - That'll still work fine if message is None, right?


def throttled(self, request, wait):
Expand Down Expand Up @@ -280,6 +282,8 @@ def check_permissions(self, request):
"""
for permission in self.get_permissions():
if not permission.has_permission(request, self):
if hasattr(permission, 'message'):
self.permission_denied(request, permission.message)
self.permission_denied(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we simplify this to self.permission_denied(request, message=getattr(permission, 'message', None))?


def check_object_permissions(self, request, obj):
Expand All @@ -289,6 +293,8 @@ def check_object_permissions(self, request, obj):
"""
for permission in self.get_permissions():
if not permission.has_object_permission(request, self, obj):
if hasattr(permission, 'message'):
self.permission_denied(request, permission.message)
self.permission_denied(request)

def check_throttles(self, request):
Expand Down
86 changes: 86 additions & 0 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,89 @@ def test_cannot_read_list_permissions(self):
response = object_permissions_list_view(request)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data, [])


class BasicPerm(permissions.BasePermission):
def has_permission(self, request, view):
return False


class BasicPermWithDetail(permissions.BasePermission):
message = 'Custom: You cannot post to this resource'

def has_permission(self, request, view):
return False


class BasicObjectPerm(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return False


class BasicObjectPermWithDetail(permissions.BasePermission):
message = 'Custom: You cannot post to this resource'

def has_object_permission(self, request, view, obj):
return False


class PermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
queryset = BasicModel.objects.all()
serializer_class = BasicSerializer


class DeniedView(PermissionInstanceView):
permission_classes = (BasicPerm,)


class DeniedViewWithDetail(PermissionInstanceView):
permission_classes = (BasicPermWithDetail,)


class DeniedObjectView(PermissionInstanceView):
permission_classes = (BasicObjectPerm,)


class DeniedObjectViewWithDetail(PermissionInstanceView):
permission_classes = (BasicObjectPermWithDetail,)

denied_view = DeniedView.as_view()

denied_view_with_detail = DeniedViewWithDetail.as_view()

denied_object_view = DeniedObjectView.as_view()

denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view()


class CustomPermissionsTests(TestCase):
def setUp(self):
BasicModel(text='foo').save()
User.objects.create_user('username', '[email protected]', 'password')
credentials = basic_auth_header('username', 'password')
self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials)
self.custom_message = 'Custom: You cannot post to this resource'

def test_permission_denied(self):
response = denied_view(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotEqual(detail, self.custom_message)

def test_permission_denied_with_custom_detail(self):
response = denied_view_with_detail(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(detail, self.custom_message)

def test_permission_denied_for_object(self):
response = denied_object_view(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotEqual(detail, self.custom_message)

def test_permission_denied_for_object_with_custom_detail(self):
response = denied_object_view_with_detail(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(detail, self.custom_message)