-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Fix BasePermission.has_object_permission() from short circuiting in permission composition. #7155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I believe this would be solved by #6605 |
why has this not been merged?, the only quick fix I could do for this type of use case where it would check is admin (has_permission) or is owner (has_object_permission) class IsStaff(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.groups.filter(name="staff").exists()
def has_object_permission(self, request, view, obj):
return request.user.groups.filter(name="staff").exists()
class IsOwner(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
"""
message = "user making the request is not the owner of this object"
def has_object_permission(self, request, view, obj):
return request.user == obj |
Hrm. Okay, but this is problematic in that it'll potentially run multiple queries for simpler cases that don't require this. Agreed with @xordoquy - #6605 looks right to me.
That's okay, if |
@tomchristie Regarding I can update #6605 but have the same hesitation @tomchristie has. |
384efe7
to
d88d06d
Compare
The original semantic of OR is defined as: the request pass either of the two has_permission() check, and pass either of the two has_object_permission() check, which could lead to situations that a request passes has_permission() but fails on has_object_permission() of Permission Class A, fails has_permission() but passes has_object_permission() of Permission Class B, passes the OR permission check. This should not be the desired permission check semantic in applications, because such a request should fail on either Permission Class (on Django object permission) alone, but passes the OR or the two. My code fix this by changing the semantic so that the request has to pass either class's has_permission() and has_object_permission() to get the Django object permission of the OR check.
Given the docs say "If you wish to use the provided permission classes in order to check object permissions, you must subclass them and implement the has_object_permission() method" you would expect that by default that function either returns False or raises NotImplemented. |
I also noticed this might affect the custom message behaviour, is there a way you make that pull request compatible with this one #6502 as well? |
Do not merge, still WIP. I've been having some tox errors locally I was trying to avoid fixing, but since I'll look at #6502 afterwards I'm going to stop now and fix it so I can stop polluting the PR. |
I believe this is now ready for review. I've already started looking at #6502 if I need to add compatibility with it. |
@tomchristie Awaiting review, resolves a milestone that was moved from 3.12 to 3.13 yesterday. |
Indeed. We had so much stuff already pending that I figured we were better off pushing on with a 3.12 release. We'll aim to have a 3.13 release without such a big delay next time. |
A better and simpler solution is to change the BasePermission.has_object_permission() to return self.has_permission() instead of True i.e.
I have implemented a custom IsAdminUser as such to fix this bug:
|
@ysliaw01 we did the same and have been running it in production for over a year, we do it in a base class that all our perm classes inherit off, of course we have to be careful to never use the built in perm classes |
@ysliaw01 It is certainly a simpler solution, and would solve the problem for many people in many cases, but I disagree that it is a better solution. If your base check is expensive it will unnecessarily be called twice. And there are also some other cases I was able to come up with in testing that this solution still doesn't get right. I still believe that a 3-value solution (allow, deny, don't-care) is more technically correct and expressive. That's what my patch does, as well as moving all the complexity into the composites (and/or/not) to keep the Permission class simple and only call has_permission as a fallback to has_object_permission in the very specific OR case where it is necessary. |
@sparkyb Can you kindly elaborate the other cases where my mentioned above solution would fail? Thank. |
It's been almost 2 years now and still no solution for this :s |
@ysliaw01 The biggest case is with NOT. Let's say I have an Beyond just that case, my other goal is to not redundantly call If just calling |
@sparkyb First thank for the explanation. But for your given example of ~IsOwner, my mentioned solution will work because BasePermission.has_object_permission() return self.has_permission() instead of True. This is the only change that is required in my solution. I like your proposed solution, which avoids the calling has_permission() twice as you said. This bug is serious and has been for quite a while, and I would like to see this being fixed asap. |
@ysliaw01 I don't think it will cover it. In a two value system with object specific things like "IsOwner"(of the object) the has_permission check fundamentally has to be broader than the actual truth, including users who are not the owner so they can be filtered by has_object_permission, in this case probably including everyone. But then to @sparkyb's point, when you invert something that is overboard it becomes over narrow, in this case excluding people who might not be the owner before we can call has_object_permission, in this case probably excluding everyone. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Description
Attempts to fix #7117
If an object permission evaluates to False while being composed with an object only implementing
.has_permission()
, then it is short circuited due to the default ofBasePermission.has_object_permission()
. An example where this can be problematic is if you are calculating an object permission and an admin permission. This is replicated in the failing test.I resolved the failing test by changing
BasePermission.has_object_permission()
to return the value ofBasePermission.has_permission()