Skip to content

Commit 319238c

Browse files
committed
Handle composition of unimplemented permission methods correctly
Previously has_object_permission defaulted to returning True when unimplemented because it was assumed that has_permission had already returned true, but when composed with OR, this might not be the case. Take for example the case where you want an object to be accessible by the user that created it or any admin. If you have a permission IsOwner that implements has_object_permission and IsAdminUser which doesn't, then if you OR them together an have a user that is neither the owner nor the admin they should get denied but instead IsOwner will pass has_permission and IsAdminUser will pass has_object_permission even though it didn't pass has_permission. One solution would be to default has_object_permission to calling has_permission but that would potentially cause redundant database lookups. Alternatively this could be done only in the OR class, but the redundant calls will still happen. Also, incorrect behavior can still occur using more complicated compositions including NOT. For example, the IsOwner permission above only implemented has_object_permission and not has_permission, but ~IsOwner will always fail, even on objects that the authenticated user doesn't own, because the default True from BasePermission.has_permission() will also be inverted. My solution is to be more explicit about when a permission subclass doesn't implement has_permission or has_object_permission by returning NotImplemented. NotImplemented is truthy in a boolean context, so by default everything will continue to work the same as long as code is not expecting the result to literally be True or False. I modified AND and OR so that if one side returns NotImplemented, it defers to the other. If both sides return NotImplemented, NotImplmented will be returned to propogate upwards. NOT will also propagate NotImplmented instead of inverting it. If NotImplemented propagates all the way up to APIView.check_permissions or APIView.check_object_permissions, it is considered a pass (truthy).
1 parent 4f4fb69 commit 319238c

File tree

1 file changed

+27
-21
lines changed

1 file changed

+27
-21
lines changed

rest_framework/permissions.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,18 @@ def __init__(self, op1, op2):
5353
self.op2 = op2
5454

5555
def has_permission(self, request, view):
56-
return (
57-
self.op1.has_permission(request, view) and
58-
self.op2.has_permission(request, view)
59-
)
56+
hasperm1 = self.op1.has_permission(request, view)
57+
if not hasperm1:
58+
return hasperm1
59+
hasperm2 = self.op2.has_permission(request, view)
60+
return hasperm1 if hasperm2 is NotImplemented else hasperm2
6061

6162
def has_object_permission(self, request, view, obj):
62-
return (
63-
self.op1.has_object_permission(request, view, obj) and
64-
self.op2.has_object_permission(request, view, obj)
65-
)
63+
hasperm1 = self.op1.has_object_permission(request, view, obj)
64+
if not hasperm1:
65+
return hasperm1
66+
hasperm2 = self.op2.has_object_permission(request, view, obj)
67+
return hasperm1 if hasperm2 is NotImplemented else hasperm2
6668

6769

6870
class OR:
@@ -71,27 +73,31 @@ def __init__(self, op1, op2):
7173
self.op2 = op2
7274

7375
def has_permission(self, request, view):
74-
return (
75-
self.op1.has_permission(request, view) or
76-
self.op2.has_permission(request, view)
77-
)
76+
hasperm1 = self.op1.has_permission(request, view)
77+
if hasperm1 and hasperm1 is not NotImplemented:
78+
return hasperm1
79+
hasperm2 = self.op2.has_permission(request, view)
80+
return hasperm1 if hasperm2 is NotImplemented else hasperm2
7881

7982
def has_object_permission(self, request, view, obj):
80-
return (
81-
self.op1.has_object_permission(request, view, obj) or
82-
self.op2.has_object_permission(request, view, obj)
83-
)
83+
hasperm1 = self.op1.has_object_permission(request, view, obj)
84+
if hasperm1 and hasperm1 is not NotImplemented:
85+
return hasperm1
86+
hasperm2 = self.op2.has_object_permission(request, view, obj)
87+
return hasperm1 if hasperm2 is NotImplemented else hasperm2
8488

8589

8690
class NOT:
8791
def __init__(self, op1):
8892
self.op1 = op1
8993

9094
def has_permission(self, request, view):
91-
return not self.op1.has_permission(request, view)
95+
hasperm = self.op1.has_permission(request, view)
96+
return hasperm if hasperm is NotImplemented else not hasperm
9297

9398
def has_object_permission(self, request, view, obj):
94-
return not self.op1.has_object_permission(request, view, obj)
99+
hasperm = self.op1.has_object_permission(request, view, obj)
100+
return hasperm if hasperm is NotImplemented else not hasperm
95101

96102

97103
class BasePermissionMetaclass(OperationHolderMixin, type):
@@ -107,13 +113,13 @@ def has_permission(self, request, view):
107113
"""
108114
Return `True` if permission is granted, `False` otherwise.
109115
"""
110-
return True
116+
return NotImplemented
111117

112118
def has_object_permission(self, request, view, obj):
113119
"""
114120
Return `True` if permission is granted, `False` otherwise.
115121
"""
116-
return True
122+
return NotImplemented
117123

118124

119125
class AllowAny(BasePermission):
@@ -220,7 +226,7 @@ def has_permission(self, request, view):
220226
# Workaround to ensure DjangoModelPermissions are not applied
221227
# to the root view when using DefaultRouter.
222228
if getattr(view, '_ignore_model_permissions', False):
223-
return True
229+
return super().has_permission(request, view)
224230

225231
if not request.user or (
226232
not request.user.is_authenticated and self.authenticated_users_only):

0 commit comments

Comments
 (0)