Add object level permission to generic view - django

The situation is pretty simple:
I'm writing a multi-user blog system. The system should prevent non-owner to edit or delete a blog post. In my view I use generic view.
class BlogUpdateView(UpdateView):
...
I know I should use #method_decorator to decorate dispatch method. However, most example is just #method_decorator(login_required) or model level permission. How can apply object level permission to check whether request.user is the author of this blog post?
For example, I tried to use django-authority apps, and I have a BlogPermission class in this file. and I tried to define a method in this class e.g.
def blog_edit(self, ??, ??)
what should I put into this method?
And then call this like:
#method_decorator(permission_required('blog_permission.blog_edit(???)'))
What should I pass in here?
Update: After read method_decorator code, I find it can only accept function without argument. I think that's why permission_required doesn't work here. But what's the work around about this?
Update solution:
In dispatch method, I check the user permission and then return HttpResponseForbidden() if the user does not meet the permission.

You can do it using class-based-views:
class BlogEdit(UpdateView):
model = Blog
def dispatch(self, request, *args, **kwargs):
if not request.user.has_perm('blog_permission.blog_edit'):
return HttpResponseForbidden()
return super(BlogEdit, self).dispatch(request, *args, **kwargs)
# OR (for object-level perms)
def get_object(self, *args, **kwargs):
obj = super(BlogEdit, self).get_object(*args, **kwargs)
if not obj.user == self.request.user:
raise Http404 # maybe you'll need to write a middleware to catch 403's same way
return obj

Another option is to use UserPassesTestMixin (or user_passes_test for function-based).
class UserPassesTestMixin
When using class-based views, you can use the
UserPassesTestMixin to do this.
test_func()
You have to override the test_func() method of the class to
provide the test that is performed. Furthermore, you can set any of
the parameters of AccessMixin to customize the handling of
unauthorized users:
from django.contrib.auth.mixins import UserPassesTestMixin
class MyView(UserPassesTestMixin, View):
def test_func(self):
return self.request.user.email.endswith('#example.com')
We can now check if the self.request.user is allowed to process the details passed into the self.request.GET or self.request.POST.
class MyView(UserPassesTestMixin, View):
raise_exception = True # To not redirect to the login url and just return 403. For the other settings, see https://docs.djangoproject.com/en/3.2/topics/auth/default/#django.contrib.auth.mixins.AccessMixin
def test_func(self):
return (
self.request.user.is_staff
or self.request.user.has_perm('app.change_blog')
or self.request.user.email.endswith('#company.staff.com')
or is_requested_object_accessible(self.request.user, self.request.GET, self.request.POST) # If you have a custom checker
)
...

Related

Django Custom Permission method or decorator

I am many views and more than one user type. I want some views that can be seen by specific user types and other users cant see this.
For example, only company see this views and for this i did that like this below:
#login_required
def only_company_can_view(request):
if not Company.objects.filter(owner_id=request.user.id).exists():
return HttpResponse('Permission Denied. Only Company can see this')
# > rest of the logic
return render(request, 'template.html')
and above this working very well and solves my problem but i don't like this. coz, i don't want to write every time for the rest of the views for the company related views.
So i am finding a solution so that i can use decorator or something else that are best practice
Can anyone help me in this case?
You can wrap the logic in a decorator:
from django.core.exceptions import PermissionDenied
from functools import wraps
def requires_company(view):
#wraps(view)
def _view(request, *args, **kwargs):
if not Company.objects.filter(owner_id=request.user.id).exists():
raise PermissionDenied
return view(request, *args, **kwargs)
return _view
Then you use the decorator with:
#login_required
#requires_company
def only_company_can_view(request):
# … rest of the logic …
return render(request, 'template.html')

Django - Permissions and DeleteView, is this a good way to do it?

Hi so I have a working view. I wanted to know if this is the best or a good way of doing it. I want to have a delete view that deletes a Photo object but only if the logged in user is the on associated with the object.
Here is my views.py
class PhotoDelete(DeleteView):
model = Photo
template_name = 'otologue/photo_delete.html'
success_url = reverse_lazy('otologue:photos')
def get(self, request, *args, **kwargs):
object_instance = self.get_object() # Get the object
object_user = object_instance.photoextended.user # Get the user who owns the object
user = get_object_or_404(User, username=self.request.user) # Get the user in the view
if object_user != user: # See if the object_user is the same as the user
return HttpResponseForbidden('Permission Error')
else:
return render(request, self.template_name, {'object': object_instance})
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
If you need more info like the models and such, please request it.
You don't have to call get_object_or_404(), since self.request.user already contains an instance of User, so you can compare it directly.
You should call (and return the result) DeleteView's get() method instead of rendering the template yourself.
I would also use LoginRequiredMixin instead of a decorator, since your dispatch() now does nothing apart from calling super's method.
Be aware, that the actual deletion is done in the post() method. You should move your user check to the dispatch() method, otherwise it will be possible to bypass the check with a forged POST request.
To have better idea what DeleteView does in its methods, check it's source code (for example at ccbv.co.uk)

Object ownership validation in Django UpdateView

EDIT:
The better solution for me was just using a permissions system, especially since I needed other types of controlled access to objects. I now use Django-guardian to help with object level permissions like this.
Original:
I'm expanding a bit on the standard django book guide by letting users upload stories, as well as having author, publisher, etc. I'm attempting to only let authors (creators) of a story use the updateview, with other users being redirected away.
Modifying get_object in the UpdateStory view set it off, but the traceback goes through my StoryForm init for some reason. The error is 'HttpResponseRedirect' object has no attribute '_meta'
views.py
class UpdateStory(LoginRequiredMixin, UpdateView):
model = Story
template_name = 'stories/story_update.html'
form_class = StoryForm
def get_object(self, queryset=None):
obj = super(UpdateStory, self).get_object()
if not obj.author == self.request.user:
return redirect(obj)
return obj
forms.py
class StoryForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(StoryForm,self).__init__(*args, **kwargs)
I'm still new, so it might be obvious, but I've been looking for a couple hours and I'm stumped.
The best approach would be to use another mixin, something like this:
class AuthorRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if self.object.author != self.request.user:
return HttpResponseForbidden()
return super(AuthorRequiredMixin, self).dispatch(request, *args, **kwargs)
Of course you can return another HttpResponse, but keep in mind what is the proper use here.
http://ccbv.co.uk/projects/Django/1.5/django.views.generic.edit/UpdateView/
Go through the above link to understand how UpdateView works. get_object is supposed to return the model instance, It is not supposed to return HttpResponseRedirect object, that's why you are getting that error.
Try doing the check in dispatch method like the following.
def dispatch(self, request, *args, **kwargs):
""" Making sure that only authors can update stories """
obj = self.get_object()
if obj.author != self.request.user:
return redirect(obj)
return super(UpdateStory, self).dispatch(request, *args, **kwargs)
PS: I guess it is not recommended to override dispatch. But as you
have to do the check on both get and post methods, overriding dispatch
will be easier.
This specific issue is considered in Django anti-patterns.
We're encouraged to filter the QuerySet to only retrieve objects where the user is the author, as opposed to UserPassesTestMixin.
In OP's case it would actually be quite similar to what they have there
from django.contrib.auth.mixins import LoginRequiredMixin
class UpdateStory(LoginRequiredMixin, UpdateView):
model = Story
# …
def get_queryset(self, *args, **kwargs):
return super().get_queryset(*args, **kwargs).filter(
author=self.request.user
)

Django Guardian - How to use a permission_required decorator with a class based view?

I have a CB DeleteView that I am trying to decorate with Guardian's permission_required. The permission should be for the logged in user and for the object of the DeleteView. The Guardian docs aren't too clear about this, so I'm wondering if anyone could clarify.
I encountered almost the same problem and here is my solution (adapted to your case):
views.py
class MyModelDeleteView(DeleteView):
model=MyModel
#method_decorator(permission_required_or_403('myapp.delete_mymodel',
(MyModel, 'slug', 'slug'), accept_global_perms=True))
def dispatch(self, *args, **kwargs):
return super(MyModelDeleteView, self).dispatch(*args, **kwargs)
Note that you can pass accept_global_perms parameter, that is False by default. It allows users with 'myapp.delete_mymodel' permission to delete any object of MyModel class. This can be useful for moderators, for example.
Guardian Decorators documentation.
To decorate every instance of a class-based view, you need to decorate the class definition itself. To do this you apply the decorator to the dispatch() method of the class.For xample,
class ExampleView(TemplateView):
template_name = 'Example.html'
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(ExampleView, self).dispatch(*args, **kwargs)

Redirect from Generic View DetailView in Django

I'm using Django's class based DetailView generic view to look up an object for display. Under certain circumstances, rather than displaying the object, I wish to back out and issue a HTTP rediect instead. I can't see how I go about doing this. It's for when a user hits an object in my app, but without using the canonical URL. So, for example, on StackOverflow URLs take the form:
http://stackoverflow.com/<content_type>/<pk>/<seo_friendly_slug>
eg:
http://stackoverflow.com/questions/5661806/django-debug-toolbar-with-django-cms-and-django-1-3
You can actually type anything as the seo_friendly_slug part and it will redirect you to the correct canonical URL for the object looked up via the PK.
I wish to do the same in my DetailView. Retrieve the object, check that it's the canonical URL, and if not redirect to the item's get_absolute_url URL.
I can't return an HttpResponseRedirect in get_object, as it's expecting the looked up object. I can't seem to return it from get_context_data, as it's just expecting context data.
Maybe I just need to write a manual view, but I wondered if anyone knew if it was possible?
Thanks!
Ludo.
This isn't a natural fit for DetailView. To do this you need to override the get method of BaseDetailView, which looks like:
class BaseDetailView(SingleObjectMixin, View):
def get(self, request, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
So in your class you'd need to provide a new get method which did the URL check between fetching the object and setting up the context. Something like:
def get(self, request, **kwargs):
self.object = self.get_object()
if self.request.path != self.object.get_absolute_url():
return HttpResponseRedirect(self.object.get_absolute_url())
else:
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
As you end up overriding so much of the functionality it becomes questionable whether it's worth actually using a generic view for this, but youknow.
Developing on Rolo's answer and comments, I came up with the following generic view to serve this purpose:
from django import http
from django.views import generic
class CanonicalDetailView(generic.DetailView):
"""
A DetailView which redirects to the absolute_url, if necessary.
"""
def get_object(self, *args, **kwargs):
# Return any previously-cached object
if getattr(self, 'object', None):
return self.object
return super(CanonicalDetailView, self).get_object(*args, **kwargs)
def get(self, *args, **kwargs):
# Make sure to use the canonical URL
self.object = self.get_object()
obj_url = self.object.get_absolute_url()
if self.request.path != obj_url:
return http.HttpResponsePermanentRedirect(obj_url)
return super(CanonicalDetailView, self).get(*args, **kwargs);
This is used in the same manner as the normal DetailView, and should work for any model which implements get_absolute_url correctly.