Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions rest_framework/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@ def __init__(self, op1, op2):
self.op2 = op2

def has_permission(self, request, view):
return (
self.op1.has_permission(request, view) and
self.op2.has_permission(request, view)
)
hasperm1 = self.op1.has_permission(request, view)
if not hasperm1:
return hasperm1
hasperm2 = self.op2.has_permission(request, view)
return hasperm1 if hasperm2 is NotImplemented else hasperm2

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)
)
hasperm1 = self.op1.has_object_permission(request, view, obj)
if not hasperm1:
return hasperm1
hasperm2 = self.op2.has_object_permission(request, view, obj)
return hasperm1 if hasperm2 is NotImplemented else hasperm2


class OR:
Expand All @@ -71,27 +73,35 @@ def __init__(self, op1, op2):
self.op2 = op2

def has_permission(self, request, view):
return (
self.op1.has_permission(request, view) or
self.op2.has_permission(request, view)
)
hasperm1 = self.op1.has_permission(request, view)
if hasperm1 and hasperm1 is not NotImplemented:
return hasperm1
hasperm2 = self.op2.has_permission(request, view)
return hasperm2 or hasperm1

def has_object_permission(self, request, view, obj):
return (
self.op1.has_object_permission(request, view, obj) or
self.op2.has_object_permission(request, view, obj)
)
hasperm1 = self.op1.has_object_permission(request, view, obj)
if hasperm1 is NotImplemented:
hasperm1 = self.op1.has_permission(request, view)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if these lines are removed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR If I wrote my tests right, if you remove these two lines and run the tests, you'll see exactly why they are needed.

Long version: If has_object_permission returns NotImplemented it should fall back on has_permission, but this is only necessary in the OR case. With a non-composed permission or an AND case, calling has_permission is redundant because you know it must have returned True in the original has_permission check to make it to check the object. But with OR, it's possible that has_permission is False and the original check passed only because of the other side of the OR. We don't want the has_object_permission test to pass for the wrong reason.

if hasperm1 and hasperm1 is not NotImplemented:
return hasperm1
hasperm2 = self.op2.has_object_permission(request, view, obj)
if hasperm2 is NotImplemented:
hasperm2 = self.op2.has_permission(request, view)
return hasperm2 or hasperm1


class NOT:
def __init__(self, op1):
self.op1 = op1

def has_permission(self, request, view):
return not self.op1.has_permission(request, view)
hasperm = self.op1.has_permission(request, view)
return hasperm if hasperm is NotImplemented else not hasperm

def has_object_permission(self, request, view, obj):
return not self.op1.has_object_permission(request, view, obj)
hasperm = self.op1.has_object_permission(request, view, obj)
return hasperm if hasperm is NotImplemented else not hasperm


class BasePermissionMetaclass(OperationHolderMixin, type):
Expand All @@ -107,13 +117,13 @@ def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
return NotImplemented

def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
return NotImplemented


class AllowAny(BasePermission):
Expand Down Expand Up @@ -220,7 +230,7 @@ def has_permission(self, request, view):
# Workaround to ensure DjangoModelPermissions are not applied
# to the root view when using DefaultRouter.
if getattr(view, '_ignore_model_permissions', False):
return True
return super().has_permission(request, view)

if not request.user or (
not request.user.is_authenticated and self.authenticated_users_only):
Expand Down
33 changes: 33 additions & 0 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,12 @@ def setUp(self):
self.password
)
self.client.login(username=self.username, password=self.password)
self.admin_user = User.objects.create_user(
'paul',
'[email protected]',
'password',
is_staff=True,
)

def test_and_false(self):
request = factory.get('/1', format='json')
Expand Down Expand Up @@ -677,3 +683,30 @@ def test_object_and_lazyness(self):
assert hasperm is False
assert mock_deny.call_count == 1
mock_allow.assert_not_called()

def test_has_permission_not_implemented(self):
request = factory.get('/1', format='json')
request.user = self.user
composed_perm = ~BasicObjectPerm
assert composed_perm().has_permission(request, None) is NotImplemented
assert composed_perm().has_object_permission(request, None, None) is True

def test_has_object_permission_not_implemented_false(self):
request = factory.get('/1', format='json')
request.user = self.user
composed_perm = (
permissions.IsAdminUser |
BasicObjectPerm
)
assert composed_perm().has_permission(request, None) is NotImplemented
assert composed_perm().has_object_permission(request, None, None) is False

def test_has_object_permission_not_implemented_true(self):
request = factory.get('/1', format='json')
request.user = self.admin_user
composed_perm = (
permissions.IsAdminUser |
BasicObjectPerm
)
assert composed_perm().has_permission(request, None) is True
assert composed_perm().has_object_permission(request, None, None) is True