From 9ea615af14303663a47fd155ab541d8302d609e4 Mon Sep 17 00:00:00 2001 From: donewell Date: Tue, 10 Feb 2015 17:41:03 +0000 Subject: [PATCH 1/3] add message to custom permission change detail to message and update text --- docs/api-guide/permissions.md | 10 ++++ rest_framework/views.py | 8 +++- tests/test_permissions.py | 86 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 743ca435c..e299b4180 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -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. diff --git a/rest_framework/views.py b/rest_framework/views.py index bc870417f..4fa97124f 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -134,12 +134,14 @@ class APIView(View): """ 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() def throttled(self, request, wait): @@ -280,6 +282,8 @@ class APIView(View): """ 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) def check_object_permissions(self, request, obj): @@ -289,6 +293,8 @@ class APIView(View): """ 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): diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 97bac33db..68d34785e 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -310,3 +310,89 @@ class ObjectPermissionsIntegrationTests(TestCase): 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', 'username@example.com', '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) From 9a9a00bff2f61d08573f764012e389e7f8e5c6ae Mon Sep 17 00:00:00 2001 From: donewell Date: Wed, 11 Feb 2015 11:15:01 +0000 Subject: [PATCH 2/3] simplify argument handling --- rest_framework/views.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 4fa97124f..435796c4e 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -140,9 +140,7 @@ class APIView(View): """ if not request.successful_authenticator: raise exceptions.NotAuthenticated() - if message is not None: - raise exceptions.PermissionDenied(message) - raise exceptions.PermissionDenied() + raise exceptions.PermissionDenied(detail=message) def throttled(self, request, wait): """ @@ -282,9 +280,9 @@ class APIView(View): """ 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) + self.permission_denied( + request, message=getattr(permission, 'message', None) + ) def check_object_permissions(self, request, obj): """ @@ -293,9 +291,9 @@ class APIView(View): """ 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) + self.permission_denied( + request, message=getattr(permission, 'message', None) + ) def check_throttles(self, request): """ From 3d25dadbf36930439f6cb5bfa81166c0ddcf0b38 Mon Sep 17 00:00:00 2001 From: donewell Date: Wed, 11 Feb 2015 11:20:03 +0000 Subject: [PATCH 3/3] change custom message for clarity --- tests/test_permissions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 68d34785e..2c0caa487 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -318,7 +318,7 @@ class BasicPerm(permissions.BasePermission): class BasicPermWithDetail(permissions.BasePermission): - message = 'Custom: You cannot post to this resource' + message = 'Custom: You cannot access this resource' def has_permission(self, request, view): return False @@ -330,7 +330,7 @@ class BasicObjectPerm(permissions.BasePermission): class BasicObjectPermWithDetail(permissions.BasePermission): - message = 'Custom: You cannot post to this resource' + message = 'Custom: You cannot access this resource' def has_object_permission(self, request, view, obj): return False @@ -371,7 +371,7 @@ class CustomPermissionsTests(TestCase): User.objects.create_user('username', 'username@example.com', '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' + self.custom_message = 'Custom: You cannot access this resource' def test_permission_denied(self): response = denied_view(self.request, pk=1)