Skip to content

Commit 59fb01e

Browse files
committed
permissions: Allow permissions to be composed
Implement a system to compose permissions with and / or. This is performed by returning an `OperationHolder` instance that keeps the permission classes and type of composition (and / or). When called it will return a AND/OR instance that will then delegate the permission check to the operands.
1 parent 2709de1 commit 59fb01e

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

rest_framework/permissions.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,76 @@
44
from __future__ import unicode_literals
55

66
from django.http import Http404
7+
from django.utils import six
78

89
from rest_framework import exceptions
910

1011
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
1112

1213

14+
class OperandHolder:
15+
def __init__(self, operator_class, op1_class, op2_class):
16+
self.operator_class = operator_class
17+
self.op1_class = op1_class
18+
self.op2_class = op2_class
19+
20+
def __call__(self, *args, **kwargs):
21+
op1 = self.op1_class(*args, **kwargs)
22+
op2 = self.op2_class(*args, **kwargs)
23+
return self.operator_class(op1, op2)
24+
25+
26+
class AND:
27+
def __init__(self, op1, op2):
28+
self.op1 = op1
29+
self.op2 = op2
30+
31+
def has_permission(self, request, view):
32+
return (
33+
self.op1.has_permission(request, view) &
34+
self.op2.has_permission(request, view)
35+
)
36+
37+
def has_object_permission(self, request, view, obj):
38+
return (
39+
self.op1.has_object_permission(request, view, obj) &
40+
self.op2.has_object_permission(request, view, obj)
41+
)
42+
43+
44+
class OR:
45+
def __init__(self, op1, op2):
46+
self.op1 = op1
47+
self.op2 = op2
48+
49+
def has_permission(self, request, view):
50+
return (
51+
self.op1.has_permission(request, view) |
52+
self.op2.has_permission(request, view)
53+
)
54+
55+
def has_object_permission(self, request, view, obj):
56+
return (
57+
self.op1.has_object_permission(request, view, obj) |
58+
self.op2.has_object_permission(request, view, obj)
59+
)
60+
61+
62+
class BasePermissionMetaclass(type):
63+
def __and__(cls, other):
64+
return OperandHolder(AND, cls, other)
65+
66+
def __or__(cls, other):
67+
return OperandHolder(OR, cls, other)
68+
69+
def __rand__(cls, other):
70+
return OperandHolder(AND, other, cls)
71+
72+
def __ror__(cls, other):
73+
return OperandHolder(OR, other, cls)
74+
75+
76+
@six.add_metaclass(BasePermissionMetaclass)
1377
class BasePermission(object):
1478
"""
1579
A base class from which all permission classes should inherit.

tests/test_permissions.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,3 +519,45 @@ def test_permission_denied_for_object_with_custom_detail(self):
519519
detail = response.data.get('detail')
520520
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
521521
self.assertEqual(detail, self.custom_message)
522+
523+
524+
class FakeUser:
525+
def __init__(self, auth=False):
526+
self.is_authenticated = auth
527+
528+
529+
class PermissionsCompositionTests(TestCase):
530+
def test_and_false(self):
531+
request = factory.get('/1', format='json')
532+
request.user = FakeUser(auth=False)
533+
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
534+
assert composed_perm().has_permission(request, None) is False
535+
536+
def test_and_true(self):
537+
request = factory.get('/1', format='json')
538+
request.user = FakeUser(auth=True)
539+
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
540+
assert composed_perm().has_permission(request, None) is True
541+
542+
def test_or_false(self):
543+
request = factory.get('/1', format='json')
544+
request.user = FakeUser(auth=False)
545+
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
546+
assert composed_perm().has_permission(request, None) is True
547+
548+
def test_or_true(self):
549+
request = factory.get('/1', format='json')
550+
request.user = FakeUser(auth=True)
551+
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
552+
assert composed_perm().has_permission(request, None) is True
553+
554+
def test_several_levels(self):
555+
request = factory.get('/1', format='json')
556+
request.user = FakeUser(auth=True)
557+
composed_perm = (
558+
permissions.IsAuthenticated &
559+
permissions.IsAuthenticated &
560+
permissions.IsAuthenticated &
561+
permissions.IsAuthenticated
562+
)
563+
assert composed_perm().has_permission(request, None) is True

0 commit comments

Comments
 (0)