From e5162e1f481d3bafcd54460145136033651b78b9 Mon Sep 17 00:00:00 2001 From: Basil Dubyk Date: Fri, 8 Mar 2019 10:31:24 +0200 Subject: [PATCH 1/7] Modify `AND` operand to have the ability to use custom messages --- rest_framework/permissions.py | 27 +++++++--- tests/test_permissions.py | 98 ++++++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 15 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 7c15eca589..6fdee5f290 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -62,18 +62,29 @@ class AND: def __init__(self, op1, op2): self.op1 = op1 self.op2 = op2 + self.message = None def has_permission(self, request, view): - return ( - self.op1.has_permission(request, view) and - self.op2.has_permission(request, view) - ) + if not self.op1.has_permission(request, view): + self.message = getattr(self.op1, 'message', None) + return False + + if not self.op2.has_permission(request, view): + self.message = getattr(self.op2, 'message', None) + return False + + return True def has_object_permission(self, request, view, obj): - return ( - self.op1.has_object_permission(request, view, obj) and - self.op2.has_object_permission(request, view, obj) - ) + if not self.op1.has_object_permission(request, view, obj): + self.message = getattr(self.op1, 'message', None) + return False + + if not self.op2.has_object_permission(request, view, obj): + self.message = getattr(self.op2, 'message', None) + return False + + return True class OR: diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 39b7ed6622..1881128159 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -18,6 +18,9 @@ factory = APIRequestFactory() +CUSTOM_MESSAGE_1 = 'Custom: You cannot access this resource' +CUSTOM_MESSAGE_2 = 'Custom: You do not have permission to view this resource' + class BasicSerializer(serializers.ModelSerializer): class Meta: @@ -454,26 +457,40 @@ def has_permission(self, request, view): class BasicPermWithDetail(permissions.BasePermission): - message = 'Custom: You cannot access this resource' + message = CUSTOM_MESSAGE_1 code = 'permission_denied_custom' def has_permission(self, request, view): return False +class AnotherBasicPermWithDetail(permissions.BasePermission): + message = CUSTOM_MESSAGE_2 + + 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 access this resource' + message = CUSTOM_MESSAGE_1 code = 'permission_denied_custom' def has_object_permission(self, request, view, obj): return False +class AnotherBasicObjectPermWithDetail(permissions.BasePermission): + message = CUSTOM_MESSAGE_2 + + def has_object_permission(self, request, view, obj): + return False + + class PermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): queryset = BasicModel.objects.all() serializer_class = BasicSerializer @@ -487,6 +504,18 @@ class DeniedViewWithDetail(PermissionInstanceView): permission_classes = (BasicPermWithDetail,) +class DeniedViewWithDetailAND1(PermissionInstanceView): + permission_classes = (BasicPermWithDetail & permissions.AllowAny,) + + +class DeniedViewWithDetailAND2(PermissionInstanceView): + permission_classes = (permissions.AllowAny & AnotherBasicPermWithDetail,) + + +class DeniedViewWithDetailAND3(PermissionInstanceView): + permission_classes = (BasicPermWithDetail & AnotherBasicPermWithDetail,) + + class DeniedObjectView(PermissionInstanceView): permission_classes = (BasicObjectPerm,) @@ -495,14 +524,34 @@ class DeniedObjectViewWithDetail(PermissionInstanceView): permission_classes = (BasicObjectPermWithDetail,) +class DeniedObjectViewWithDetailAND1(PermissionInstanceView): + permission_classes = (BasicObjectPermWithDetail & permissions.AllowAny,) + + +class DeniedObjectViewWithDetailAND2(PermissionInstanceView): + permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail,) + + +class DeniedObjectViewWithDetailAND3(PermissionInstanceView): + permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail,) + + denied_view = DeniedView.as_view() denied_view_with_detail = DeniedViewWithDetail.as_view() +denied_view_with_detail_and_1 = DeniedViewWithDetailAND1.as_view() +denied_view_with_detail_and_2 = DeniedViewWithDetailAND2.as_view() +denied_view_with_detail_and_3 = DeniedViewWithDetailAND3.as_view() + denied_object_view = DeniedObjectView.as_view() denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view() +denied_object_view_with_detail_and_1 = DeniedObjectViewWithDetailAND1.as_view() +denied_object_view_with_detail_and_2 = DeniedObjectViewWithDetailAND2.as_view() +denied_object_view_with_detail_and_3 = DeniedObjectViewWithDetailAND3.as_view() + class CustomPermissionsTests(TestCase): def setUp(self): @@ -510,37 +559,72 @@ def setUp(self): 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 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, CUSTOM_MESSAGE_1) 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, CUSTOM_MESSAGE_1) self.assertEqual(detail.code, self.custom_code) + def test_permission_denied_with_custom_detail_and_1(self): + response = denied_view_with_detail_and_1(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + + def test_permission_denied_with_custom_detail_and_2(self): + response = denied_view_with_detail_and_2(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_2) + + def test_permission_denied_with_custom_detail_and_3(self): + response = denied_view_with_detail_and_3(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + 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, CUSTOM_MESSAGE_1) 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, CUSTOM_MESSAGE_1) self.assertEqual(detail.code, self.custom_code) + def test_permission_denied_for_object_with_custom_detail_and_1(self): + response = denied_object_view_with_detail_and_1(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + + def test_permission_denied_for_object_with_custom_detail_and_2(self): + response = denied_object_view_with_detail_and_2(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_2) + + def test_permission_denied_for_object_with_custom_detail_and_3(self): + response = denied_object_view_with_detail_and_3(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_2) + class PermissionsCompositionTests(TestCase): From 42976d50511be604f734478d8ae7e40775ed269d Mon Sep 17 00:00:00 2001 From: Basil Dubyk Date: Sat, 9 Mar 2019 13:11:01 +0200 Subject: [PATCH 2/7] Modify `OR` operand to have the ability to use custom messages --- rest_framework/permissions.py | 8 ++++ tests/test_permissions.py | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6fdee5f290..b426546a79 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -2,6 +2,7 @@ Provides a set of pluggable permission policies. """ from django.http import Http404 +from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions @@ -91,6 +92,13 @@ class OR: def __init__(self, op1, op2): self.op1 = op1 self.op2 = op2 + self.message1 = getattr(op1, 'message', None) + self.message2 = getattr(op2, 'message', None) + self.message = self.message1 or self.message2 + if self.message1 and self.message2: + self.message = '"{0}" {1} "{2}"'.format( + self.message1, _('OR'), self.message2, + ) def has_permission(self, request, view): return ( diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 1881128159..59009de684 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -516,6 +516,18 @@ class DeniedViewWithDetailAND3(PermissionInstanceView): permission_classes = (BasicPermWithDetail & AnotherBasicPermWithDetail,) +class DeniedViewWithDetailOR1(PermissionInstanceView): + permission_classes = (BasicPerm | BasicPermWithDetail,) + + +class DeniedViewWithDetailOR2(PermissionInstanceView): + permission_classes = (BasicPermWithDetail | BasicPerm,) + + +class DeniedViewWithDetailOR3(PermissionInstanceView): + permission_classes = (BasicPermWithDetail | AnotherBasicPermWithDetail,) + + class DeniedObjectView(PermissionInstanceView): permission_classes = (BasicObjectPerm,) @@ -536,6 +548,18 @@ class DeniedObjectViewWithDetailAND3(PermissionInstanceView): permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail,) +class DeniedObjectViewWithDetailOR1(PermissionInstanceView): + permission_classes = (BasicObjectPerm | BasicObjectPermWithDetail,) + + +class DeniedObjectViewWithDetailOR2(PermissionInstanceView): + permission_classes = (BasicObjectPermWithDetail | BasicObjectPerm,) + + +class DeniedObjectViewWithDetailOR3(PermissionInstanceView): + permission_classes = (BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail,) + + denied_view = DeniedView.as_view() denied_view_with_detail = DeniedViewWithDetail.as_view() @@ -544,6 +568,10 @@ class DeniedObjectViewWithDetailAND3(PermissionInstanceView): denied_view_with_detail_and_2 = DeniedViewWithDetailAND2.as_view() denied_view_with_detail_and_3 = DeniedViewWithDetailAND3.as_view() +denied_view_with_detail_or_1 = DeniedViewWithDetailOR1.as_view() +denied_view_with_detail_or_2 = DeniedViewWithDetailOR2.as_view() +denied_view_with_detail_or_3 = DeniedViewWithDetailOR3.as_view() + denied_object_view = DeniedObjectView.as_view() denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view() @@ -552,6 +580,10 @@ class DeniedObjectViewWithDetailAND3(PermissionInstanceView): denied_object_view_with_detail_and_2 = DeniedObjectViewWithDetailAND2.as_view() denied_object_view_with_detail_and_3 = DeniedObjectViewWithDetailAND3.as_view() +denied_object_view_with_detail_or_1 = DeniedObjectViewWithDetailOR1.as_view() +denied_object_view_with_detail_or_2 = DeniedObjectViewWithDetailOR2.as_view() +denied_object_view_with_detail_or_3 = DeniedObjectViewWithDetailOR3.as_view() + class CustomPermissionsTests(TestCase): def setUp(self): @@ -593,6 +625,25 @@ def test_permission_denied_with_custom_detail_and_3(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(detail, CUSTOM_MESSAGE_1) + def test_permission_denied_with_custom_detail_or_1(self): + response = denied_view_with_detail_or_1(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + + def test_permission_denied_with_custom_detail_or_2(self): + response = denied_view_with_detail_or_2(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + + def test_permission_denied_with_custom_detail_or_3(self): + response = denied_view_with_detail_or_3(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) + self.assertEqual(detail, expected_message) + def test_permission_denied_for_object(self): response = denied_object_view(self.request, pk=1) detail = response.data.get('detail') @@ -625,6 +676,25 @@ def test_permission_denied_for_object_with_custom_detail_and_3(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(detail, CUSTOM_MESSAGE_2) + def test_permission_denied_for_object_with_custom_detail_or_1(self): + response = denied_object_view_with_detail_or_1(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + + def test_permission_denied_for_object_with_custom_detail_or_2(self): + response = denied_object_view_with_detail_or_2(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, CUSTOM_MESSAGE_1) + + def test_permission_denied_for_object_with_custom_detail_or_3(self): + response = denied_object_view_with_detail_or_3(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) + self.assertEqual(detail, expected_message) + class PermissionsCompositionTests(TestCase): From a3ac11ea487a716c22ac505676b67db4e068d8b6 Mon Sep 17 00:00:00 2001 From: Basil Dubyk Date: Mon, 13 Apr 2020 18:45:56 +0300 Subject: [PATCH 3/7] Modify `NOT` operand to have the ability to use custom message --- rest_framework/permissions.py | 1 + tests/test_permissions.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index b426546a79..dc4a3f0b86 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -119,6 +119,7 @@ def has_object_permission(self, request, view, obj): class NOT: def __init__(self, op1): self.op1 = op1 + self.message = getattr(self.op1, 'message_inverted', None) def has_permission(self, request, view): return not self.op1.has_permission(request, view) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 59009de684..fbe11970e6 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -20,6 +20,7 @@ CUSTOM_MESSAGE_1 = 'Custom: You cannot access this resource' CUSTOM_MESSAGE_2 = 'Custom: You do not have permission to view this resource' +INVERTED_MESSAGE = 'Inverted: Your account already active' class BasicSerializer(serializers.ModelSerializer): @@ -458,6 +459,7 @@ def has_permission(self, request, view): class BasicPermWithDetail(permissions.BasePermission): message = CUSTOM_MESSAGE_1 + message_inverted = INVERTED_MESSAGE code = 'permission_denied_custom' def has_permission(self, request, view): @@ -478,6 +480,7 @@ def has_object_permission(self, request, view, obj): class BasicObjectPermWithDetail(permissions.BasePermission): message = CUSTOM_MESSAGE_1 + message_inverted = INVERTED_MESSAGE code = 'permission_denied_custom' def has_object_permission(self, request, view, obj): @@ -528,6 +531,10 @@ class DeniedViewWithDetailOR3(PermissionInstanceView): permission_classes = (BasicPermWithDetail | AnotherBasicPermWithDetail,) +class DeniedViewWithDetailNOT(PermissionInstanceView): + permission_classes = (~BasicPermWithDetail,) + + class DeniedObjectView(PermissionInstanceView): permission_classes = (BasicObjectPerm,) @@ -560,6 +567,10 @@ class DeniedObjectViewWithDetailOR3(PermissionInstanceView): permission_classes = (BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail,) +class DeniedObjectViewWithDetailNOT(PermissionInstanceView): + permission_classes = (~BasicObjectPermWithDetail,) + + denied_view = DeniedView.as_view() denied_view_with_detail = DeniedViewWithDetail.as_view() @@ -572,6 +583,8 @@ class DeniedObjectViewWithDetailOR3(PermissionInstanceView): denied_view_with_detail_or_2 = DeniedViewWithDetailOR2.as_view() denied_view_with_detail_or_3 = DeniedViewWithDetailOR3.as_view() +denied_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view() + denied_object_view = DeniedObjectView.as_view() denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view() @@ -584,6 +597,8 @@ class DeniedObjectViewWithDetailOR3(PermissionInstanceView): denied_object_view_with_detail_or_2 = DeniedObjectViewWithDetailOR2.as_view() denied_object_view_with_detail_or_3 = DeniedObjectViewWithDetailOR3.as_view() +denied_object_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view() + class CustomPermissionsTests(TestCase): def setUp(self): @@ -644,6 +659,12 @@ def test_permission_denied_with_custom_detail_or_3(self): expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) self.assertEqual(detail, expected_message) + def test_permission_denied_with_custom_detail_not(self): + response = denied_view_with_detail_not(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, INVERTED_MESSAGE) + def test_permission_denied_for_object(self): response = denied_object_view(self.request, pk=1) detail = response.data.get('detail') @@ -695,6 +716,12 @@ def test_permission_denied_for_object_with_custom_detail_or_3(self): expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) self.assertEqual(detail, expected_message) + def test_permission_denied_for_object_with_custom_detail_not(self): + response = denied_object_view_with_detail_not(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, INVERTED_MESSAGE) + class PermissionsCompositionTests(TestCase): From 2856523c6ce7417d71f5a838a80ffefa580d934f Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 12 Apr 2023 22:09:00 +0600 Subject: [PATCH 4/7] Update tests/test_permissions.py --- tests/test_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index fbe11970e6..79bc27bdc0 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -552,7 +552,7 @@ class DeniedObjectViewWithDetailAND2(PermissionInstanceView): class DeniedObjectViewWithDetailAND3(PermissionInstanceView): - permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail,) + permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail) class DeniedObjectViewWithDetailOR1(PermissionInstanceView): From 8ffa898196dd3522744ea1ac28c396cc92a05ed0 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 12 Apr 2023 22:09:17 +0600 Subject: [PATCH 5/7] Update tests/test_permissions.py --- tests/test_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 79bc27bdc0..865e88b7be 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -556,7 +556,7 @@ class DeniedObjectViewWithDetailAND3(PermissionInstanceView): class DeniedObjectViewWithDetailOR1(PermissionInstanceView): - permission_classes = (BasicObjectPerm | BasicObjectPermWithDetail,) + permission_classes = (BasicObjectPerm | BasicObjectPermWithDetail) class DeniedObjectViewWithDetailOR2(PermissionInstanceView): From dbdcb2039f11cae265ef6f0ddc06bd89ec863ea8 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 12 Apr 2023 22:09:26 +0600 Subject: [PATCH 6/7] Update tests/test_permissions.py --- tests/test_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 865e88b7be..dabef46e79 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -548,7 +548,7 @@ class DeniedObjectViewWithDetailAND1(PermissionInstanceView): class DeniedObjectViewWithDetailAND2(PermissionInstanceView): - permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail,) + permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail) class DeniedObjectViewWithDetailAND3(PermissionInstanceView): From 431f8fe3aef31e16e67d6bc26cb17149fcf05b8b Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Tue, 11 Feb 2025 11:07:34 +0200 Subject: [PATCH 7/7] refactor: Refactor permissions to allow list --- rest_framework/exceptions.py | 11 ++++ rest_framework/permissions.py | 93 +++++++++++++++-------------- tests/test_permissions.py | 109 ++++++++++++++-------------------- 3 files changed, 107 insertions(+), 106 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 09f111102e..af1f4df1d1 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -106,6 +106,17 @@ class APIException(Exception): default_code = 'error' def __init__(self, detail=None, code=None): + if ( + isinstance(detail, tuple) + and isinstance(code, tuple) + and len(detail) == len(code) + ): + self.detail = [ + _get_error_details(d or self.default_detail, c or self.default_code) + for d, c in zip(detail, code) + ] + return + if detail is None: detail = self.default_detail if code is None: diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index dc4a3f0b86..b5b5789711 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -2,7 +2,6 @@ Provides a set of pluggable permission policies. """ from django.http import Http404 -from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions @@ -59,61 +58,69 @@ def __hash__(self): return hash((self.operator_class, self.op1_class, self.op2_class)) -class AND: - def __init__(self, op1, op2): - self.op1 = op1 - self.op2 = op2 - self.message = None +class OperatorBase: + def __init__(self, *permissions): + self._permissions = permissions - def has_permission(self, request, view): - if not self.op1.has_permission(request, view): - self.message = getattr(self.op1, 'message', None) - return False - if not self.op2.has_permission(request, view): - self.message = getattr(self.op2, 'message', None) - return False +class AND(OperatorBase): + def has_permission(self, request, view): + for perm in self._permissions: + if not perm.has_permission(request, view): + self._set_message_and_code(perm) + return False return True def has_object_permission(self, request, view, obj): - if not self.op1.has_object_permission(request, view, obj): - self.message = getattr(self.op1, 'message', None) - return False - - if not self.op2.has_object_permission(request, view, obj): - self.message = getattr(self.op2, 'message', None) - return False - + for perm in self._permissions: + if not perm.has_object_permission(request, view, obj): + self._set_message_and_code(perm) + return False return True + def _set_message_and_code(self, perm): + self.message = getattr(perm, 'message', None) + self.code = getattr(perm, 'code', None) -class OR: - def __init__(self, op1, op2): - self.op1 = op1 - self.op2 = op2 - self.message1 = getattr(op1, 'message', None) - self.message2 = getattr(op2, 'message', None) - self.message = self.message1 or self.message2 - if self.message1 and self.message2: - self.message = '"{0}" {1} "{2}"'.format( - self.message1, _('OR'), self.message2, - ) + +class OR(OperatorBase): def has_permission(self, request, view): - return ( - self.op1.has_permission(request, view) or - self.op2.has_permission(request, view) - ) + collector = ResultCollector() + for perm in self._permissions: + if perm.has_permission(request, view): + return True + else: + collector.add_message_and_code(perm) + collector.finalize(self) + return False def has_object_permission(self, request, view, obj): - return ( - self.op1.has_permission(request, view) - and self.op1.has_object_permission(request, view, obj) - ) or ( - self.op2.has_permission(request, view) - and self.op2.has_object_permission(request, view, obj) - ) + collector = ResultCollector() + for perm in self._permissions: + if perm.has_permission(request, view) and perm.has_object_permission(request, view, obj): + return True + else: + collector.add_message_and_code(perm) + collector.finalize(self) + return False + + +class ResultCollector: + def __init__(self): + self.messages = () + self.codes = () + + def add_message_and_code(self, perm): + message = getattr(perm, 'message', None) + code = getattr(perm, 'code', None) + self.messages += (message,) + self.codes += (code,) + + def finalize(self, perm): + perm.message = self.messages + perm.code = self.codes class NOT: diff --git a/tests/test_permissions.py b/tests/test_permissions.py index dabef46e79..a9e7272537 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -12,14 +12,16 @@ HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status, views ) +from rest_framework.exceptions import ErrorDetail from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory from tests.models import BasicModel factory = APIRequestFactory() -CUSTOM_MESSAGE_1 = 'Custom: You cannot access this resource' -CUSTOM_MESSAGE_2 = 'Custom: You do not have permission to view this resource' +DEFAULT_MESSAGE = ErrorDetail('You do not have permission to perform this action.', 'permission_denied') +CUSTOM_MESSAGE_1 = ErrorDetail('Custom: You cannot access this resource', 'permission_denied_custom') +CUSTOM_MESSAGE_2 = ErrorDetail('Custom: You do not have permission to view this resource', 'permission_denied_custom') INVERTED_MESSAGE = 'Inverted: Your account already active' @@ -519,18 +521,6 @@ class DeniedViewWithDetailAND3(PermissionInstanceView): permission_classes = (BasicPermWithDetail & AnotherBasicPermWithDetail,) -class DeniedViewWithDetailOR1(PermissionInstanceView): - permission_classes = (BasicPerm | BasicPermWithDetail,) - - -class DeniedViewWithDetailOR2(PermissionInstanceView): - permission_classes = (BasicPermWithDetail | BasicPerm,) - - -class DeniedViewWithDetailOR3(PermissionInstanceView): - permission_classes = (BasicPermWithDetail | AnotherBasicPermWithDetail,) - - class DeniedViewWithDetailNOT(PermissionInstanceView): permission_classes = (~BasicPermWithDetail,) @@ -548,23 +538,11 @@ class DeniedObjectViewWithDetailAND1(PermissionInstanceView): class DeniedObjectViewWithDetailAND2(PermissionInstanceView): - permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail) + permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail,) class DeniedObjectViewWithDetailAND3(PermissionInstanceView): - permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail) - - -class DeniedObjectViewWithDetailOR1(PermissionInstanceView): - permission_classes = (BasicObjectPerm | BasicObjectPermWithDetail) - - -class DeniedObjectViewWithDetailOR2(PermissionInstanceView): - permission_classes = (BasicObjectPermWithDetail | BasicObjectPerm,) - - -class DeniedObjectViewWithDetailOR3(PermissionInstanceView): - permission_classes = (BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail,) + permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail,) class DeniedObjectViewWithDetailNOT(PermissionInstanceView): @@ -579,10 +557,6 @@ class DeniedObjectViewWithDetailNOT(PermissionInstanceView): denied_view_with_detail_and_2 = DeniedViewWithDetailAND2.as_view() denied_view_with_detail_and_3 = DeniedViewWithDetailAND3.as_view() -denied_view_with_detail_or_1 = DeniedViewWithDetailOR1.as_view() -denied_view_with_detail_or_2 = DeniedViewWithDetailOR2.as_view() -denied_view_with_detail_or_3 = DeniedViewWithDetailOR3.as_view() - denied_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view() denied_object_view = DeniedObjectView.as_view() @@ -593,10 +567,6 @@ class DeniedObjectViewWithDetailNOT(PermissionInstanceView): denied_object_view_with_detail_and_2 = DeniedObjectViewWithDetailAND2.as_view() denied_object_view_with_detail_and_3 = DeniedObjectViewWithDetailAND3.as_view() -denied_object_view_with_detail_or_1 = DeniedObjectViewWithDetailOR1.as_view() -denied_object_view_with_detail_or_2 = DeniedObjectViewWithDetailOR2.as_view() -denied_object_view_with_detail_or_3 = DeniedObjectViewWithDetailOR3.as_view() - denied_object_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view() @@ -606,21 +576,18 @@ def setUp(self): 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_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, CUSTOM_MESSAGE_1) - self.assertNotEqual(detail.code, self.custom_code) + self.assertEqual(detail, DEFAULT_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, CUSTOM_MESSAGE_1) - self.assertEqual(detail.code, self.custom_code) def test_permission_denied_with_custom_detail_and_1(self): response = denied_view_with_detail_and_1(self.request, pk=1) @@ -641,23 +608,31 @@ def test_permission_denied_with_custom_detail_and_3(self): self.assertEqual(detail, CUSTOM_MESSAGE_1) def test_permission_denied_with_custom_detail_or_1(self): - response = denied_view_with_detail_or_1(self.request, pk=1) - detail = response.data.get('detail') + view = PermissionInstanceView.as_view( + permission_classes=(BasicPerm | BasicPermWithDetail,), + ) + response = view(self.request, pk=1) + detail = response.data self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(detail, CUSTOM_MESSAGE_1) + self.assertEqual(detail, [DEFAULT_MESSAGE, CUSTOM_MESSAGE_1]) def test_permission_denied_with_custom_detail_or_2(self): - response = denied_view_with_detail_or_2(self.request, pk=1) - detail = response.data.get('detail') + view = PermissionInstanceView.as_view( + permission_classes=(BasicPermWithDetail | BasicPerm,), + ) + response = view(self.request, pk=1) + detail = response.data self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(detail, CUSTOM_MESSAGE_1) + self.assertEqual(detail, [CUSTOM_MESSAGE_1, DEFAULT_MESSAGE]) def test_permission_denied_with_custom_detail_or_3(self): - response = denied_view_with_detail_or_3(self.request, pk=1) - detail = response.data.get('detail') + view = PermissionInstanceView.as_view( + permission_classes=(BasicPermWithDetail | AnotherBasicPermWithDetail,), + ) + response = view(self.request, pk=1) + detail = response.data self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) - self.assertEqual(detail, expected_message) + self.assertEqual(detail, [CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2]) def test_permission_denied_with_custom_detail_not(self): response = denied_view_with_detail_not(self.request, pk=1) @@ -669,15 +644,13 @@ 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, CUSTOM_MESSAGE_1) - self.assertNotEqual(detail.code, self.custom_code) + self.assertEqual(detail, DEFAULT_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, CUSTOM_MESSAGE_1) - self.assertEqual(detail.code, self.custom_code) def test_permission_denied_for_object_with_custom_detail_and_1(self): response = denied_object_view_with_detail_and_1(self.request, pk=1) @@ -698,23 +671,33 @@ def test_permission_denied_for_object_with_custom_detail_and_3(self): self.assertEqual(detail, CUSTOM_MESSAGE_2) def test_permission_denied_for_object_with_custom_detail_or_1(self): - response = denied_object_view_with_detail_or_1(self.request, pk=1) - detail = response.data.get('detail') + view = PermissionInstanceView.as_view( + permission_classes=(BasicObjectPerm | BasicObjectPermWithDetail,), + ) + response = view(self.request, pk=1) + detail = response.data self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(detail, CUSTOM_MESSAGE_1) + self.assertEqual(detail, [DEFAULT_MESSAGE, CUSTOM_MESSAGE_1]) def test_permission_denied_for_object_with_custom_detail_or_2(self): - response = denied_object_view_with_detail_or_2(self.request, pk=1) - detail = response.data.get('detail') + view = PermissionInstanceView.as_view( + permission_classes=(BasicObjectPermWithDetail | BasicObjectPerm,), + ) + response = view(self.request, pk=1) + detail = response.data self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(detail, CUSTOM_MESSAGE_1) + self.assertEqual(detail, [CUSTOM_MESSAGE_1, DEFAULT_MESSAGE]) def test_permission_denied_for_object_with_custom_detail_or_3(self): - response = denied_object_view_with_detail_or_3(self.request, pk=1) - detail = response.data.get('detail') + view = PermissionInstanceView.as_view( + permission_classes=( + BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail, + ), + ) + response = view(self.request, pk=1) + detail = response.data self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) - self.assertEqual(detail, expected_message) + self.assertEqual(detail, [CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2]) def test_permission_denied_for_object_with_custom_detail_not(self): response = denied_object_view_with_detail_not(self.request, pk=1)