refactor: Refactor permissions to allow list

This commit is contained in:
Serhii Tereshchenko 2025-02-11 11:07:34 +02:00
parent dbdcb2039f
commit 431f8fe3ae
3 changed files with 108 additions and 107 deletions

View File

@ -106,6 +106,17 @@ class APIException(Exception):
default_code = 'error' default_code = 'error'
def __init__(self, detail=None, code=None): 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: if detail is None:
detail = self.default_detail detail = self.default_detail
if code is None: if code is None:

View File

@ -2,7 +2,6 @@
Provides a set of pluggable permission policies. Provides a set of pluggable permission policies.
""" """
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions from rest_framework import exceptions
@ -59,61 +58,69 @@ class OperandHolder(OperationHolderMixin):
return hash((self.operator_class, self.op1_class, self.op2_class)) return hash((self.operator_class, self.op1_class, self.op2_class))
class AND: class OperatorBase:
def __init__(self, op1, op2): def __init__(self, *permissions):
self.op1 = op1 self._permissions = permissions
self.op2 = op2
self.message = None
class AND(OperatorBase):
def has_permission(self, request, view): def has_permission(self, request, view):
if not self.op1.has_permission(request, view): for perm in self._permissions:
self.message = getattr(self.op1, 'message', None) if not perm.has_permission(request, view):
return False self._set_message_and_code(perm)
return False
if not self.op2.has_permission(request, view):
self.message = getattr(self.op2, 'message', None)
return False
return True return True
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
if not self.op1.has_object_permission(request, view, obj): for perm in self._permissions:
self.message = getattr(self.op1, 'message', None) if not perm.has_object_permission(request, view, obj):
return False self._set_message_and_code(perm)
return False
if not self.op2.has_object_permission(request, view, obj):
self.message = getattr(self.op2, 'message', None)
return False
return True 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): class OR(OperatorBase):
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): def has_permission(self, request, view):
return ( collector = ResultCollector()
self.op1.has_permission(request, view) or for perm in self._permissions:
self.op2.has_permission(request, view) 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): def has_object_permission(self, request, view, obj):
return ( collector = ResultCollector()
self.op1.has_permission(request, view) for perm in self._permissions:
and self.op1.has_object_permission(request, view, obj) if perm.has_permission(request, view) and perm.has_object_permission(request, view, obj):
) or ( return True
self.op2.has_permission(request, view) else:
and self.op2.has_object_permission(request, view, obj) 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: class NOT:

View File

@ -12,14 +12,16 @@ from rest_framework import (
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
status, views status, views
) )
from rest_framework.exceptions import ErrorDetail
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from tests.models import BasicModel from tests.models import BasicModel
factory = APIRequestFactory() factory = APIRequestFactory()
CUSTOM_MESSAGE_1 = 'Custom: You cannot access this resource' DEFAULT_MESSAGE = ErrorDetail('You do not have permission to perform this action.', 'permission_denied')
CUSTOM_MESSAGE_2 = 'Custom: You do not have permission to view this resource' 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' INVERTED_MESSAGE = 'Inverted: Your account already active'
@ -519,18 +521,6 @@ class DeniedViewWithDetailAND3(PermissionInstanceView):
permission_classes = (BasicPermWithDetail & AnotherBasicPermWithDetail,) 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): class DeniedViewWithDetailNOT(PermissionInstanceView):
permission_classes = (~BasicPermWithDetail,) permission_classes = (~BasicPermWithDetail,)
@ -548,23 +538,11 @@ class DeniedObjectViewWithDetailAND1(PermissionInstanceView):
class DeniedObjectViewWithDetailAND2(PermissionInstanceView): class DeniedObjectViewWithDetailAND2(PermissionInstanceView):
permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail) permission_classes = (permissions.AllowAny & AnotherBasicObjectPermWithDetail,)
class DeniedObjectViewWithDetailAND3(PermissionInstanceView): class DeniedObjectViewWithDetailAND3(PermissionInstanceView):
permission_classes = (AnotherBasicObjectPermWithDetail & BasicObjectPermWithDetail) 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,)
class DeniedObjectViewWithDetailNOT(PermissionInstanceView): class DeniedObjectViewWithDetailNOT(PermissionInstanceView):
@ -579,10 +557,6 @@ denied_view_with_detail_and_1 = DeniedViewWithDetailAND1.as_view()
denied_view_with_detail_and_2 = DeniedViewWithDetailAND2.as_view() denied_view_with_detail_and_2 = DeniedViewWithDetailAND2.as_view()
denied_view_with_detail_and_3 = DeniedViewWithDetailAND3.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_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view()
denied_object_view = DeniedObjectView.as_view() denied_object_view = DeniedObjectView.as_view()
@ -593,10 +567,6 @@ 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_2 = DeniedObjectViewWithDetailAND2.as_view()
denied_object_view_with_detail_and_3 = DeniedObjectViewWithDetailAND3.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() denied_object_view_with_detail_not = DeniedObjectViewWithDetailNOT.as_view()
@ -606,21 +576,18 @@ class CustomPermissionsTests(TestCase):
User.objects.create_user('username', 'username@example.com', 'password') User.objects.create_user('username', 'username@example.com', 'password')
credentials = basic_auth_header('username', 'password') credentials = basic_auth_header('username', 'password')
self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials) self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials)
self.custom_code = 'permission_denied_custom'
def test_permission_denied(self): def test_permission_denied(self):
response = denied_view(self.request, pk=1) response = denied_view(self.request, pk=1)
detail = response.data.get('detail') detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotEqual(detail, CUSTOM_MESSAGE_1) self.assertEqual(detail, DEFAULT_MESSAGE)
self.assertNotEqual(detail.code, self.custom_code)
def test_permission_denied_with_custom_detail(self): def test_permission_denied_with_custom_detail(self):
response = denied_view_with_detail(self.request, pk=1) response = denied_view_with_detail(self.request, pk=1)
detail = response.data.get('detail') detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(detail, CUSTOM_MESSAGE_1) self.assertEqual(detail, CUSTOM_MESSAGE_1)
self.assertEqual(detail.code, self.custom_code)
def test_permission_denied_with_custom_detail_and_1(self): def test_permission_denied_with_custom_detail_and_1(self):
response = denied_view_with_detail_and_1(self.request, pk=1) response = denied_view_with_detail_and_1(self.request, pk=1)
@ -641,23 +608,31 @@ class CustomPermissionsTests(TestCase):
self.assertEqual(detail, CUSTOM_MESSAGE_1) self.assertEqual(detail, CUSTOM_MESSAGE_1)
def test_permission_denied_with_custom_detail_or_1(self): def test_permission_denied_with_custom_detail_or_1(self):
response = denied_view_with_detail_or_1(self.request, pk=1) view = PermissionInstanceView.as_view(
detail = response.data.get('detail') permission_classes=(BasicPerm | BasicPermWithDetail,),
)
response = view(self.request, pk=1)
detail = response.data
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 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): def test_permission_denied_with_custom_detail_or_2(self):
response = denied_view_with_detail_or_2(self.request, pk=1) view = PermissionInstanceView.as_view(
detail = response.data.get('detail') permission_classes=(BasicPermWithDetail | BasicPerm,),
)
response = view(self.request, pk=1)
detail = response.data
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 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): def test_permission_denied_with_custom_detail_or_3(self):
response = denied_view_with_detail_or_3(self.request, pk=1) view = PermissionInstanceView.as_view(
detail = response.data.get('detail') permission_classes=(BasicPermWithDetail | AnotherBasicPermWithDetail,),
)
response = view(self.request, pk=1)
detail = response.data
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) self.assertEqual(detail, [CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2])
self.assertEqual(detail, expected_message)
def test_permission_denied_with_custom_detail_not(self): def test_permission_denied_with_custom_detail_not(self):
response = denied_view_with_detail_not(self.request, pk=1) response = denied_view_with_detail_not(self.request, pk=1)
@ -669,15 +644,13 @@ class CustomPermissionsTests(TestCase):
response = denied_object_view(self.request, pk=1) response = denied_object_view(self.request, pk=1)
detail = response.data.get('detail') detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotEqual(detail, CUSTOM_MESSAGE_1) self.assertEqual(detail, DEFAULT_MESSAGE)
self.assertNotEqual(detail.code, self.custom_code)
def test_permission_denied_for_object_with_custom_detail(self): def test_permission_denied_for_object_with_custom_detail(self):
response = denied_object_view_with_detail(self.request, pk=1) response = denied_object_view_with_detail(self.request, pk=1)
detail = response.data.get('detail') detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(detail, CUSTOM_MESSAGE_1) 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): def test_permission_denied_for_object_with_custom_detail_and_1(self):
response = denied_object_view_with_detail_and_1(self.request, pk=1) response = denied_object_view_with_detail_and_1(self.request, pk=1)
@ -698,23 +671,33 @@ class CustomPermissionsTests(TestCase):
self.assertEqual(detail, CUSTOM_MESSAGE_2) self.assertEqual(detail, CUSTOM_MESSAGE_2)
def test_permission_denied_for_object_with_custom_detail_or_1(self): def test_permission_denied_for_object_with_custom_detail_or_1(self):
response = denied_object_view_with_detail_or_1(self.request, pk=1) view = PermissionInstanceView.as_view(
detail = response.data.get('detail') permission_classes=(BasicObjectPerm | BasicObjectPermWithDetail,),
)
response = view(self.request, pk=1)
detail = response.data
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 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): def test_permission_denied_for_object_with_custom_detail_or_2(self):
response = denied_object_view_with_detail_or_2(self.request, pk=1) view = PermissionInstanceView.as_view(
detail = response.data.get('detail') permission_classes=(BasicObjectPermWithDetail | BasicObjectPerm,),
)
response = view(self.request, pk=1)
detail = response.data
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 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): def test_permission_denied_for_object_with_custom_detail_or_3(self):
response = denied_object_view_with_detail_or_3(self.request, pk=1) view = PermissionInstanceView.as_view(
detail = response.data.get('detail') permission_classes=(
BasicObjectPermWithDetail | AnotherBasicObjectPermWithDetail,
),
)
response = view(self.request, pk=1)
detail = response.data
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
expected_message = '"{0}" OR "{1}"'.format(CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2) self.assertEqual(detail, [CUSTOM_MESSAGE_1, CUSTOM_MESSAGE_2])
self.assertEqual(detail, expected_message)
def test_permission_denied_for_object_with_custom_detail_not(self): def test_permission_denied_for_object_with_custom_detail_not(self):
response = denied_object_view_with_detail_not(self.request, pk=1) response = denied_object_view_with_detail_not(self.request, pk=1)