Skip to content

Commit 431f8fe

Browse files
committed
refactor: Refactor permissions to allow list
1 parent dbdcb20 commit 431f8fe

File tree

3 files changed

+107
-106
lines changed

3 files changed

+107
-106
lines changed

rest_framework/exceptions.py

+11
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ class APIException(Exception):
106106
default_code = 'error'
107107

108108
def __init__(self, detail=None, code=None):
109+
if (
110+
isinstance(detail, tuple)
111+
and isinstance(code, tuple)
112+
and len(detail) == len(code)
113+
):
114+
self.detail = [
115+
_get_error_details(d or self.default_detail, c or self.default_code)
116+
for d, c in zip(detail, code)
117+
]
118+
return
119+
109120
if detail is None:
110121
detail = self.default_detail
111122
if code is None:

rest_framework/permissions.py

+50-43
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Provides a set of pluggable permission policies.
33
"""
44
from django.http import Http404
5-
from django.utils.translation import gettext_lazy as _
65

76
from rest_framework import exceptions
87

@@ -59,61 +58,69 @@ def __hash__(self):
5958
return hash((self.operator_class, self.op1_class, self.op2_class))
6059

6160

62-
class AND:
63-
def __init__(self, op1, op2):
64-
self.op1 = op1
65-
self.op2 = op2
66-
self.message = None
61+
class OperatorBase:
62+
def __init__(self, *permissions):
63+
self._permissions = permissions
6764

68-
def has_permission(self, request, view):
69-
if not self.op1.has_permission(request, view):
70-
self.message = getattr(self.op1, 'message', None)
71-
return False
7265

73-
if not self.op2.has_permission(request, view):
74-
self.message = getattr(self.op2, 'message', None)
75-
return False
66+
class AND(OperatorBase):
7667

68+
def has_permission(self, request, view):
69+
for perm in self._permissions:
70+
if not perm.has_permission(request, view):
71+
self._set_message_and_code(perm)
72+
return False
7773
return True
7874

7975
def has_object_permission(self, request, view, obj):
80-
if not self.op1.has_object_permission(request, view, obj):
81-
self.message = getattr(self.op1, 'message', None)
82-
return False
83-
84-
if not self.op2.has_object_permission(request, view, obj):
85-
self.message = getattr(self.op2, 'message', None)
86-
return False
87-
76+
for perm in self._permissions:
77+
if not perm.has_object_permission(request, view, obj):
78+
self._set_message_and_code(perm)
79+
return False
8880
return True
8981

82+
def _set_message_and_code(self, perm):
83+
self.message = getattr(perm, 'message', None)
84+
self.code = getattr(perm, 'code', None)
9085

91-
class OR:
92-
def __init__(self, op1, op2):
93-
self.op1 = op1
94-
self.op2 = op2
95-
self.message1 = getattr(op1, 'message', None)
96-
self.message2 = getattr(op2, 'message', None)
97-
self.message = self.message1 or self.message2
98-
if self.message1 and self.message2:
99-
self.message = '"{0}" {1} "{2}"'.format(
100-
self.message1, _('OR'), self.message2,
101-
)
86+
87+
class OR(OperatorBase):
10288

10389
def has_permission(self, request, view):
104-
return (
105-
self.op1.has_permission(request, view) or
106-
self.op2.has_permission(request, view)
107-
)
90+
collector = ResultCollector()
91+
for perm in self._permissions:
92+
if perm.has_permission(request, view):
93+
return True
94+
else:
95+
collector.add_message_and_code(perm)
96+
collector.finalize(self)
97+
return False
10898

10999
def has_object_permission(self, request, view, obj):
110-
return (
111-
self.op1.has_permission(request, view)
112-
and self.op1.has_object_permission(request, view, obj)
113-
) or (
114-
self.op2.has_permission(request, view)
115-
and self.op2.has_object_permission(request, view, obj)
116-
)
100+
collector = ResultCollector()
101+
for perm in self._permissions:
102+
if perm.has_permission(request, view) and perm.has_object_permission(request, view, obj):
103+
return True
104+
else:
105+
collector.add_message_and_code(perm)
106+
collector.finalize(self)
107+
return False
108+
109+
110+
class ResultCollector:
111+
def __init__(self):
112+
self.messages = ()
113+
self.codes = ()
114+
115+
def add_message_and_code(self, perm):
116+
message = getattr(perm, 'message', None)
117+
code = getattr(perm, 'code', None)
118+
self.messages += (message,)
119+
self.codes += (code,)
120+
121+
def finalize(self, perm):
122+
perm.message = self.messages
123+
perm.code = self.codes
117124

118125

119126
class NOT:

tests/test_permissions.py

+46-63
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
1313
status, views
1414
)
15+
from rest_framework.exceptions import ErrorDetail
1516
from rest_framework.routers import DefaultRouter
1617
from rest_framework.test import APIRequestFactory
1718
from tests.models import BasicModel
1819

1920
factory = APIRequestFactory()
2021

21-
CUSTOM_MESSAGE_1 = 'Custom: You cannot access this resource'
22-
CUSTOM_MESSAGE_2 = 'Custom: You do not have permission to view this resource'
22+
DEFAULT_MESSAGE = ErrorDetail('You do not have permission to perform this action.', 'permission_denied')
23+
CUSTOM_MESSAGE_1 = ErrorDetail('Custom: You cannot access this resource', 'permission_denied_custom')
24+
CUSTOM_MESSAGE_2 = ErrorDetail('Custom: You do not have permission to view this resource', 'permission_denied_custom')
2325
INVERTED_MESSAGE = 'Inverted: Your account already active'
2426

2527

@@ -519,18 +521,6 @@ class DeniedViewWithDetailAND3(PermissionInstanceView):
519521
permission_classes = (BasicPermWithDetail & AnotherBasicPermWithDetail,)
520522

521523

522-
class DeniedViewWithDetailOR1(PermissionInstanceView):
523-
permission_classes = (BasicPerm | BasicPermWithDetail,)
524-
525-
526-
class DeniedViewWithDetailOR2(PermissionInstanceView):
527-
permission_classes = (BasicPermWithDetail | BasicPerm,)
528-
529-
530-
class DeniedViewWithDetailOR3(PermissionInstanceView):
531-
permission_classes = (BasicPermWithDetail | AnotherBasicPermWithDetail,)
532-
533-
534524
class DeniedViewWithDetailNOT(PermissionInstanceView):
535525
permission_classes = (~BasicPermWithDetail,)
536526

@@ -548,23 +538,11 @@ class DeniedObjectViewWithDetailAND1(PermissionInstanceView):
548538

549539

550540
class DeniedObjectViewWithDetailAND2(PermissionInstanceView):
551-
permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail)
541+
permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail,)
552542

553543

554544
class DeniedObjectViewWithDetailAND3(PermissionInstanceView):
555-
permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail)
556-
557-
558-
class DeniedObjectViewWithDetailOR1(PermissionInstanceView):
559-
permission_classes = (BasicObjectPerm | BasicObjectPermWithDetail)
560-
561-
562-
class DeniedObjectViewWithDetailOR2(PermissionInstanceView):
563-
permission_classes = (BasicObjectPermWithDetail | BasicObjectPerm,)
564-
565-
566-
class DeniedObjectViewWithDetailOR3(PermissionInstanceView):
567-
permission_classes = (BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail,)
545+
permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail,)
568546

569547

570548
class DeniedObjectViewWithDetailNOT(PermissionInstanceView):
@@ -579,10 +557,6 @@ class DeniedObjectViewWithDetailNOT(PermissionInstanceView):
579557
denied_view_with_detail_and_2 = DeniedViewWithDetailAND2.as_view()
580558
denied_view_with_detail_and_3 = DeniedViewWithDetailAND3.as_view()
581559

582-
denied_view_with_detail_or_1 = DeniedViewWithDetailOR1.as_view()
583-
denied_view_with_detail_or_2 = DeniedViewWithDetailOR2.as_view()
584-
denied_view_with_detail_or_3 = DeniedViewWithDetailOR3.as_view()
585-
586560
denied_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view()
587561

588562
denied_object_view = DeniedObjectView.as_view()
@@ -593,10 +567,6 @@ class DeniedObjectViewWithDetailNOT(PermissionInstanceView):
593567
denied_object_view_with_detail_and_2 = DeniedObjectViewWithDetailAND2.as_view()
594568
denied_object_view_with_detail_and_3 = DeniedObjectViewWithDetailAND3.as_view()
595569

596-
denied_object_view_with_detail_or_1 = DeniedObjectViewWithDetailOR1.as_view()
597-
denied_object_view_with_detail_or_2 = DeniedObjectViewWithDetailOR2.as_view()
598-
denied_object_view_with_detail_or_3 = DeniedObjectViewWithDetailOR3.as_view()
599-
600570
denied_object_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view()
601571

602572

@@ -606,21 +576,18 @@ def setUp(self):
606576
User.objects.create_user('username', '[email protected]', 'password')
607577
credentials = basic_auth_header('username', 'password')
608578
self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials)
609-
self.custom_code = 'permission_denied_custom'
610579

611580
def test_permission_denied(self):
612581
response = denied_view(self.request, pk=1)
613582
detail = response.data.get('detail')
614583
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
615-
self.assertNotEqual(detail, CUSTOM_MESSAGE_1)
616-
self.assertNotEqual(detail.code, self.custom_code)
584+
self.assertEqual(detail, DEFAULT_MESSAGE)
617585

618586
def test_permission_denied_with_custom_detail(self):
619587
response = denied_view_with_detail(self.request, pk=1)
620588
detail = response.data.get('detail')
621589
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
622590
self.assertEqual(detail, CUSTOM_MESSAGE_1)
623-
self.assertEqual(detail.code, self.custom_code)
624591

625592
def test_permission_denied_with_custom_detail_and_1(self):
626593
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):
641608
self.assertEqual(detail, CUSTOM_MESSAGE_1)
642609

643610
def test_permission_denied_with_custom_detail_or_1(self):
644-
response = denied_view_with_detail_or_1(self.request, pk=1)
645-
detail = response.data.get('detail')
611+
view = PermissionInstanceView.as_view(
612+
permission_classes=(BasicPerm | BasicPermWithDetail,),
613+
)
614+
response = view(self.request, pk=1)
615+
detail = response.data
646616
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
647-
self.assertEqual(detail, CUSTOM_MESSAGE_1)
617+
self.assertEqual(detail, [DEFAULT_MESSAGE, CUSTOM_MESSAGE_1])
648618

649619
def test_permission_denied_with_custom_detail_or_2(self):
650-
response = denied_view_with_detail_or_2(self.request, pk=1)
651-
detail = response.data.get('detail')
620+
view = PermissionInstanceView.as_view(
621+
permission_classes=(BasicPermWithDetail | BasicPerm,),
622+
)
623+
response = view(self.request, pk=1)
624+
detail = response.data
652625
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
653-
self.assertEqual(detail, CUSTOM_MESSAGE_1)
626+
self.assertEqual(detail, [CUSTOM_MESSAGE_1, DEFAULT_MESSAGE])
654627

655628
def test_permission_denied_with_custom_detail_or_3(self):
656-
response = denied_view_with_detail_or_3(self.request, pk=1)
657-
detail = response.data.get('detail')
629+
view = PermissionInstanceView.as_view(
630+
permission_classes=(BasicPermWithDetail | AnotherBasicPermWithDetail,),
631+
)
632+
response = view(self.request, pk=1)
633+
detail = response.data
658634
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
659-
expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2)
660-
self.assertEqual(detail, expected_message)
635+
self.assertEqual(detail, [CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2])
661636

662637
def test_permission_denied_with_custom_detail_not(self):
663638
response = denied_view_with_detail_not(self.request, pk=1)
@@ -669,15 +644,13 @@ def test_permission_denied_for_object(self):
669644
response = denied_object_view(self.request, pk=1)
670645
detail = response.data.get('detail')
671646
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
672-
self.assertNotEqual(detail, CUSTOM_MESSAGE_1)
673-
self.assertNotEqual(detail.code, self.custom_code)
647+
self.assertEqual(detail, DEFAULT_MESSAGE)
674648

675649
def test_permission_denied_for_object_with_custom_detail(self):
676650
response = denied_object_view_with_detail(self.request, pk=1)
677651
detail = response.data.get('detail')
678652
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
679653
self.assertEqual(detail, CUSTOM_MESSAGE_1)
680-
self.assertEqual(detail.code, self.custom_code)
681654

682655
def test_permission_denied_for_object_with_custom_detail_and_1(self):
683656
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):
698671
self.assertEqual(detail, CUSTOM_MESSAGE_2)
699672

700673
def test_permission_denied_for_object_with_custom_detail_or_1(self):
701-
response = denied_object_view_with_detail_or_1(self.request, pk=1)
702-
detail = response.data.get('detail')
674+
view = PermissionInstanceView.as_view(
675+
permission_classes=(BasicObjectPerm | BasicObjectPermWithDetail,),
676+
)
677+
response = view(self.request, pk=1)
678+
detail = response.data
703679
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
704-
self.assertEqual(detail, CUSTOM_MESSAGE_1)
680+
self.assertEqual(detail, [DEFAULT_MESSAGE, CUSTOM_MESSAGE_1])
705681

706682
def test_permission_denied_for_object_with_custom_detail_or_2(self):
707-
response = denied_object_view_with_detail_or_2(self.request, pk=1)
708-
detail = response.data.get('detail')
683+
view = PermissionInstanceView.as_view(
684+
permission_classes=(BasicObjectPermWithDetail | BasicObjectPerm,),
685+
)
686+
response = view(self.request, pk=1)
687+
detail = response.data
709688
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
710-
self.assertEqual(detail, CUSTOM_MESSAGE_1)
689+
self.assertEqual(detail, [CUSTOM_MESSAGE_1, DEFAULT_MESSAGE])
711690

712691
def test_permission_denied_for_object_with_custom_detail_or_3(self):
713-
response = denied_object_view_with_detail_or_3(self.request, pk=1)
714-
detail = response.data.get('detail')
692+
view = PermissionInstanceView.as_view(
693+
permission_classes=(
694+
BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail,
695+
),
696+
)
697+
response = view(self.request, pk=1)
698+
detail = response.data
715699
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
716-
expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2)
717-
self.assertEqual(detail, expected_message)
700+
self.assertEqual(detail, [CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2])
718701

719702
def test_permission_denied_for_object_with_custom_detail_not(self):
720703
response = denied_object_view_with_detail_not(self.request, pk=1)

0 commit comments

Comments
 (0)