I'm wondering how I might accomplish a simple 'object ownership' system with django models, such that, by default, only the owner of an object may edit it.
I am attempting to allow a 'Management' group to edit all objects on behalf of the object owners, and have at this point added a custom permission:
class Meta:
permissions = (
("manage_object", "Can manage objects"),
)
To establish 'ownership' I've toyed with the idea of adding a def to the model:
def owner(self):
return self.user
But then, how might I go further? I could implement the permissions in a view and display relevant UI with a template, i.e.:
if request.user is object.owner:
# ... do stuff
elseif request.user.has_perm.can_manage: # this line is probably not right
# ... do something else
... and then present different UI elements on a template level.
So, the question is:
what faults/benefits are there with this approach?
are there recommendations?
or, any other previously implement methods?
Best thanks!
My approach would be adding a method to the model:
class YourModelWithOwnership(models.model):
...
def user_can_manage_me(self, user):
return user == self.user or user.has_perm('your_app.manage_object')
I'd then call that method whenever a permission check is required, and take some action based on the outcome. So for a view that would be
from django.shortcuts import get_object_or_404
...
def view_func(request, item_id):
item = get_object_or_404(YourModelWithOwnership, id=item_id) # or whatever is needed to get the object
if not item.user_can_manage_me(request.user):
# user not allowed to manage
...
else:
...
Later I'd probably realize that that's still quite some boilerplate code to write in every view that needs that test, so I'd implement an exception that's thrown when a user can't manage an object...
class CannotManage(Exception):
pass
...and add another method to the model:
from django.db import models
from django.shortcuts import get_object_or_404
class YourModelWithOwnership(models.model):
...
#classmethod
def get_manageable_object_or_404(cls, user, *args, **kwds):
item = get_object_or_404(cls, *args, **kwds)
if not item.user_can_manage_me(user):
raise CannotManage
return item
Then, in the view functions, this can be used:
def view_func(request, item_id):
item = YourModelWithOwnership.get_manageable_object_or_404(request.user, id=item_id)
...
This will of course raise an exception when the user isn't the owner and does not have the proper permission. That exception can be handled in the process_exception() method of a custom middleware class so that there's a single handler for all instances where a user is not allowed to mess with the object.
A while back I wrote up the usual technique for doing this in the admin. You may want to read through that to see how the implementation works.
You can look into RowLevelPermissions branch. It hasn't been included even in 1.1 beta though, I guess it still needs some development.
Related
I defined a view function with the #api_view decorator. Now I would like to test object-level permissions in this object.
I know there is a check_object_permissions that would let me check the permissions I defined on my view, but I don't know how to call it. The documentation gives examples for classes, not functions.
#api_view(["GET"])
#permission_classes([IsAuthenticated & IsOwnerOfItem])
def query_something_related_to_items(request, item_id):
item = get_object_or_404(Item, id=item_id)
# I want to test IsOwnerOfItem here,
# I guess I need to call something like:
# "self".check_object_permissions(request, item)
# custom code here
return something
class IsOwnerOfItem(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
the documentation says:
If you're writing your own views and want to enforce object level permissions, or if you override the get_object method on a generic view, then you'll need to explicitly call the .check_object_permissions(request, obj) method on the view at the point at which you've retrieved the object.
But how do you call this in an view function?
Part of the documentation you referred says:
Object level permissions are run by REST framework's generic views
when .get_object()
So, class based views are meant in there. In order to check object permissions in function based view, you should write your logic and raise PermissionDenied if you decide that request shouldn't access item. Pseudocode:
from rest_framework.exceptions import PermissionDenied
# I want to test IsOwnerOfItem here,
# I guess I need to call something like:
# `has_permission_for_item` is your own function
if not has_permission_for_item(request, item):
raise PermissionDenied()
I am writing a class-based view to allow users to edit their profile. Since I want the users to access this view with a URL of the type my_profile/edit/ rather than something like profile/<int:pk>/edit/, using a view based on UpdateView is quite cumbersome since getting the user profile object requires to access the request object, and get_object does not directly have access to it.
My two questions:
Shall I use UpdateView in this case?
If so, what would be the best way to override get_object(self, queryset=None)? My best attempt so far is the following:
class EditProfileView(UpdateView):
model = UserProfile
_request = None
def get_object(self, queryset=None):
return get_user_profile(self._request)
def dispatch(self, request, *args, **kwargs): # can also override setup() in newer Django versions
self._request = request
return super().dispatch(request, *args, **kwargs)
This looks clean enough to me except that if one day the Django framework decides that get_object should be called early in the workflow of the view generation, this piece of code could break.
You don't need to do this. All class-based views make the request available as self.request.
Your entire code should just be:
class EditProfileView(UpdateView):
model = UserProfile
def get_object(self, queryset=None):
return get_user_profile(self.request)
Note, even if it didn't, you still wouldn't need to define _request at class level. That's just not necessary in Python.
Also, I don't know what your get_user_profile function does but it can probably be replaced with just self.request.user.profile.
I want to write a Mixin (or use a third-party one) for checking if the logged user is the owner of certain object.
url(r'^api/mymodel/(?P<pk>\d)/?', CreateUpdateMyModel.as_view(), name='create_or_update')
class MyModel(models.Model):
owner = models.OneToOneField('auth.User')
class OwnerRequired(SingleObjectMixin):
# do this self.object = self.get_object() for getting the object
#
# some logic for checking if request.user == self.object.owner
# otherwise return something like Response(status=status.HTTP_403_FORBIDDEN)
Inheriting from SingleObjectMixin is important to me, since I want to be able to do something like:
class CreateUpdateMyModel(APIView, OwnerRequired):
model = MyModel
def post(self, request, *args, **kwargs):
# self.object should be available here
# so that write some code taking it into account
How OwnerRequired should look for fulfilling this?
I'm open to another alternatives, in fact, I've checked PermissionRequiredMixin from django-braces and I'd like to use it, but I'm not sure how to do it
permission_required = ?? # I can code a method for, but how can I pass the model instance and the request.user?
Is there another simple alternative?
Take a look at object level permissions. There's also a relevant example on that page in the examples sections - see the IsOwnerOrReadOnly example.
Also note that object level permissions are only run either:
You are using the GenericAPIView or a subclass of it, and calling get_object() to retrieve the instance.
You explicitly call self.check_object_permissions(request, instance within your view code.
For my site I created an abstract Model which implements model-level read permissions. That part of the system is completed and works correctly. One of the methods the permissioned model exposes is is_safe(user) which can manually test if a user is allowed to view that model or not.
What I would like to do is add a method to the effect of continue_if_safe which can be called on any model instance, and instead of returning a boolean value like is_safe it would first test if the model can be viewed or not, then in the case of False, it would redirect the user, either to the login page if they aren't already logged in or return a 403 error if they are logged in.
Ideal usage:
model = get_object_or_404(Model, slug=slug)
model.continue_if_safe(request.user)
# ... remainder of code to be run if it's safe down here ...
I peeked at how the get_object_or_404 works, and it throws an Http404 error which seems to make sense. However, the problem is that there don't seem to be equivalent redirect or 403 errors. What's the best way to go about this?
(non-working) continue_if_safe method:
def continue_if_safe(self, user):
if not self.is_safe(user):
if user.is_authenticated():
raise HttpResponseForbidden()
else:
raise HttpResponseRedirect('/account/')
return
Edit -- The Solution
The code for the final solution, in case other "stackers" need some help with this:
In the Abstract Model:
def continue_if_safe(self, user):
if not self.is_safe(user):
raise PermissionDenied()
return
Views are caught by the middleware:
class PermissionDeniedToLoginMiddleware(object):
def process_exception(self, request, exception):
if type(exception) == PermissionDenied:
if not request.user.is_authenticated():
return HttpResponseRedirect('/account/?next=' + request.path)
return None
Usage in the view (very short and sweet):
model = get_object_or_404(Model, slug=slug)
model.continue_if_safe(request.user)
For the forbidden (403) error, you could raise a PermissionDenied exception (from django.core.exceptions).
For the redirecting behaviour, there's no built-in way to deal with it the way you describe in your question. You could write a custom middleware that will catch your exception and redirect in process_exception.
I've made a little middleware that return whatever your Exception class's render method returns. Now you can throw custom exception's (with render methods) in any of your views.
class ProductNotFound(Exception):
def render(self, request):
return HttpResponse("You could ofcourse use render_to_response or any response object")
pip install -e git+http://github.com/jonasgeiregat/django-excepted.git#egg=django_excepted
And add django_excepted.middleware.ExceptionHandlingMiddleware to your MIDDLEWARE_CLASSES.
Django annoying has a solution for this:
from annoying.exceptions import Redirect
...
raise Redirect('/') # or a url name, etc
You want to use decorators for this. Look up login_required for an example. Basically the decorator will allow you check check the safety and then return HttpResponseRedirect() if its not safe.
Your code will end up looking something like this:
#safety_check:
def some_view(request):
#Do Stuff
I am using django-voting as a voting application for two of my models. Those both models have fields "author".
How can I restrict a user from voting on a model that has this particular user set as it's author without modifying django-voting app?
Django middleware is the first thing that comes to my mind, but I don't understand it's "proces_view" function. If you think middleware is the right way could you please give an example of how to do it.
Add this code anywhere in your settings.py:
from voting.managers import VoteManager
def check_user(func):
def wrapper(self, obj, user, vote):
if obj.user != user:
return func(self, obj, user, vote)
else:
return None
# or raise some exception
return wrapper
VoteManager.record_vote = check_user(VoteManager.record_vote)
I didn't run this code, maybe it's incorrect, but I hope idea is clear
Rather than a middleware hack, why not reroute requests to that particular URI through another view? Then you can performs whatever logic you like, and subsequently call the original view if appropriate.
Another idea is to use the post_save signal
like so:
from django.db.models.signals import post_save
from voting.models import Vote
def check_user(sender, instance, **kwargs):
if instance.user == instance.object.user:
instance.delete()
# do some other stuff to tell the user it didn't work
post_save.connect(check_user, sender=Vote)
The benefit of doing this vs overriding VoteManager.record_vote is that it's less tightly coupled to the voting module, and if they make changes it's less likely to break your code
edit: as in Glader's answer, you need to make sure that all the objects you're voting on have a 'user' attribute.