Repository: encode/django-rest-framework

Issue:
Permission also need code property
## Checklist

- [v ] I have verified that that issue exists against the `master` branch of Django REST framework.
- [ v] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [v] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [v] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
- [v] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

as you know, each permission_denied is not the same.
and maybe each permission_denied require different behavior.
so, DRF enables override the message of Permission.
but, when communicating with clients that are independent of Django. 
(like SPA, Android, IOS)
i think code is better than a message.   
because error handling is the client's responsibility.
so, Permission also needs to override code property.






Discussion:
I think we'd probably consider a pull request adding support for an optional `code` associated with permissions, yup.

Resolving patch:
diff --git a/rest_framework/views.py b/rest_framework/views.py
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -166,13 +166,13 @@ def http_method_not_allowed(self, request, *args, **kwargs):
         """
         raise exceptions.MethodNotAllowed(request.method)
 
-    def permission_denied(self, request, message=None):
+    def permission_denied(self, request, message=None, code=None):
         """
         If request is not permitted, determine what kind of exception to raise.
         """
         if request.authenticators and not request.successful_authenticator:
             raise exceptions.NotAuthenticated()
-        raise exceptions.PermissionDenied(detail=message)
+        raise exceptions.PermissionDenied(detail=message, code=code)
 
     def throttled(self, request, wait):
         """
@@ -331,7 +331,9 @@ def check_permissions(self, request):
         for permission in self.get_permissions():
             if not permission.has_permission(request, self):
                 self.permission_denied(
-                    request, message=getattr(permission, 'message', None)
+                    request,
+                    message=getattr(permission, 'message', None),
+                    code=getattr(permission, 'code', None)
                 )
 
     def check_object_permissions(self, request, obj):
@@ -342,7 +344,9 @@ def check_object_permissions(self, request, obj):
         for permission in self.get_permissions():
             if not permission.has_object_permission(request, self, obj):
                 self.permission_denied(
-                    request, message=getattr(permission, 'message', None)
+                    request,
+                    message=getattr(permission, 'message', None),
+                    code=getattr(permission, 'code', None)
                 )
 
     def check_throttles(self, request):


Test patch:
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -438,6 +438,7 @@ def has_permission(self, request, view):
 
 class BasicPermWithDetail(permissions.BasePermission):
     message = 'Custom: You cannot access this resource'
+    code = 'permission_denied_custom'
 
     def has_permission(self, request, view):
         return False
@@ -450,6 +451,7 @@ def has_object_permission(self, request, view, obj):
 
 class BasicObjectPermWithDetail(permissions.BasePermission):
     message = 'Custom: You cannot access this resource'
+    code = 'permission_denied_custom'
 
     def has_object_permission(self, request, view, obj):
         return False
@@ -492,30 +494,35 @@ def setUp(self):
         credentials = basic_auth_header('username', 'password')
         self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials)
         self.custom_message = 'Custom: You cannot access this resource'
+        self.custom_code = 'permission_denied_custom'
 
     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)
+        self.assertNotEqual(detail.code, self.custom_code)
 
     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)
+        self.assertEqual(detail.code, self.custom_code)
 
     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)
+        self.assertNotEqual(detail.code, self.custom_code)
 
     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)
+        self.assertEqual(detail.code, self.custom_code)
 
 
 class PermissionsCompositionTests(TestCase):