Creating custom Exceptions that Django reacts to - django

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

Related

How to check_object_permissions on #api_view functions?

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()

Correct implementation of UserPassesTestMixin or AccessMixin in class based view

When using class-based views, you can use the UserPassesTestMixin in the following way (docs):
from django.contrib.auth.mixins import UserPassesTestMixin
class MyView(UserPassesTestMixin, View):
def test_func(self):
return self.request.user.email.endswith('#example.com')
The docs also state:
Furthermore, you can set any of the parameters of AccessMixin to customize the handling of unauthorized users
However, no example of the syntax is provided as to how to do this. For example, how would I set raise_exception and permission_denied_message so authenticated users get an exception error and not a 403?
This does not work, as users are still returned a 403 error:
class MyView(UserPassesTestMixin, View):
raise_exception = True
permission_denied_message = 'You have been denied'
def test_func(self):
return self.request.user.email.endswith('#example.com')
You should override the handle_no_permission method to handle what the user sees when it does not pass the test:
class MyView(UserPassesTestMixin, View):
def test_func(self):
return self.request.user.email.endswith('#example.com')
def handle_no_permission(self):
""" Do whatever you want here if the user doesn't pass the test """
return HttpResponse('You have been denied')
Class-based view UserPassesTestMixin inherited from the AccessMixin mixin which has denoted options. From the documentation of raise_exception.
If this attribute is set to True, a PermissionDenied exception is
raised when the conditions are not met. When False (the default),
anonymous users are redirected to the login page.
As you set it to True and a user wasn't authenticated the 403 status would be returned by the server. Option permission_denied_message only controls the message that would be passed to the PermissionDenied exception. It does not return any messages to a user (of course you can override it). A little bit simplified version from the source code:
def handle_no_permission(self):
if self.raise_exception or self.request.user.is_authenticated:
raise PermissionDenied(self.permission_denied_message)
return redirect_to_login(...)
Where PermissionDenied class as simple as that:
class PermissionDenied(Exception):
"""The user did not have permission to do that"""
pass

Can PermissionRequiredMixin and LoginRequiredMixin be combined?

I have some users that are allowed to see a certain view.
To allow users to login and complain with a 403 Forbidden for those users that cannot see that login, I can use the following (as explained here):
#permission_required('polls.can_vote', raise_exception=True)
#login_required
def my_view(request):
...
This indeed works as expected. But all my views are class-based views. Since Django 1.9 (finally!) there are a bunch of pretty mixins for doing things that were only possible through the decorators. However...
class MyClassView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
raise_exception = <???>
permission_required = 'polls.can_vote'
template_name = 'poll_vote.html'
this doesn't work. Because the raise_exception flag is used by both LoginRequiredMixin and PermissionRequiredMixin, I cannot set it to anything.
if raise_exception is True, a user that is not logged in receives a 403 Forbidden (which I do not want).
if raise_exception is False, a user that is not allowed to see the view, will be redirected to the login page which, because the user is logged in, will redirect again to the page. Creating a not-at-all fancy redirect loop.
Of course I could implement my own mixin that behaves I expected, but is there any Django-way of doing this in the view itself? (not in the urls.py)
For many cases raising 403 for unauthenticated users is the expected behaviour. So yes, you need a custom mixin:
class LoggedInPermissionsMixin(PermissionRequiredMixin):
def dispatch(self, request, *args, **kwargs):
if not self.request.user.is_authenticated():
return redirect_to_login(self.request.get_full_path(),
self.get_login_url(), self.get_redirect_field_name())
if not self.has_permission():
# We could also use "return self.handle_no_permission()" here
raise PermissionDenied(self.get_permission_denied_message())
return super(LoggedInPermissionsMixin, self).dispatch(request, *args, **kwargs)
The desired behavior is the default since 2.1, so the other answers are obsolete:
Changed in 2.1: In older versions, authenticated users who lacked permissions were redirected to the login page (which resulted in a loop) instead of receiving an HTTP 403 Forbidden response. [src]
I wanted to add a comment, but my reputation does not allow. How about the following? I feel the below is more readable?
Updated after comments
My reasoning is: You basically write modified dispatch from LoginRequiredMixin and just set raise_exception = True. PermissionRequiredMixin will raise PermissionDenied when correct permissions are not met
class LoggedInPermissionsMixin(PermissionRequiredMixin):
raise_exception = True
def dispatch(self, request, *args, **kwargs):
if not self.request.user.is_authenticated():
return redirect_to_login(self.request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name())
return super(LoggedInPermissionsMixin, self).dispatch(request, *args, **kwargs)
Simplest solution seems to be a custom view mixin.
Something like that:
class PermissionsMixin(PermissionRequiredMixin):
def handle_no_permission(self):
self.raise_exception = self.request.user.is_authenticated()
return super(PermissionsMixin, self).handle_no_permission()
Or, just use PermissionRequiredMixin as usual and put this handle_no_premission to every CBV.

TemplateView in Django with custom status code

I have internal account privacy permissions in my project(e.g. only friends can see profile page of user) and I want to have custom permission denied page for this case. Is there any way to return response from TemplateView with status code equals 403?
Something like this:
class PrivacyDeniedView(TempateView):
template_name = '...'
status_code = 403
I can do this by override dispatch() but maybe Django has out of the box solution
Answer: it looks like there no generic solution. The best way is proposed by #alecxe, but encapsulated in Mixin as #FoxMaSk proposed
One option is to override get() method of your TemplateView class:
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context, status=403)
You can subclass TemplateResponse and set response_class in the view. For example:
from django.template.response import TemplateResponse
class TemplateResponseForbidden(TemplateResponse):
status_code = 403
class PrivacyDeniedView(TemplateView):
response_class = TemplateResponseForbidden
...
This approach is more DRY than the other suggestions because you don't need to copy and paste any code from TemplateView (e.g. the call to render_to_string()).
I tested this in Django 1.6.
While alecxe's answer works, I strongly suggest you to avoid overriding get; it's easy to forget that CBV's can have other methods like post, and if you're overriding one you should do the same for the others.
In fact, there is no need to create a separate view just to display a 403 error; Django already has django.http.HttpResponseForbidden. So instead of redirecting to your view, just do something along the lines of:
if not user.has_permission(): # or however you check the permission
return HttpResponseForbidden()
Or, if you want to render a particular template:
if not user.has_permission(): # or however you check the permission
return HttpResponseForbidden(loader.render_to_string("403.html"))
I just encountered this problem as well. My goal was to be able to specify the status code in urls.py, e.g.:
url(r'^login/error/?$', TemplateView.as_view(template_name='auth/login_error.html', status=503), name='login_error'),
So using the previous answers in this thread as idea starters, I came up with the following solution:
class TemplateView(django.views.generic.TemplateView):
status = 200
def render_to_response(self, context, **response_kwargs):
response_kwargs['status'] = self.status
return super(TemplateView, self).render_to_response(context, **response_kwargs)
This is an old question, but I came across it first when searching. I was using a different generic view (ListView) whose implementation for get is a bit more complex, and thus a bit of a mess to override. I went for this solution instead:
def get(self, request, *args, **kwargs):
response = super(ListView, self).get(request, *args, **kwargs)
response.status_code = 403
return response
This also allowed me to decide the status code based on some parameters of the request, which was necessary in my case to perform a 301 redirect for old-style URL patterns.
If you don't require the ability to change status code from within the method, then cjerdonek's answer is ideal.

Tastypie-django custom error handling

I would like to return some JSON responses back instead of just returning a header with an error code. Is there a way in tastypie to handle errors like that?
Figured it out eventually. Here's a good resource to look at, if anyone else needs it. http://gist.github.com/1116962
class YourResource(ModelResource):
def wrap_view(self, view):
"""
Wraps views to return custom error codes instead of generic 500's
"""
#csrf_exempt
def wrapper(request, *args, **kwargs):
try:
callback = getattr(self, view)
response = callback(request, *args, **kwargs)
if request.is_ajax():
patch_cache_control(response, no_cache=True)
# response is a HttpResponse object, so follow Django's instructions
# to change it to your needs before you return it.
# https://docs.djangoproject.com/en/dev/ref/request-response/
return response
except (BadRequest, ApiFieldError), e:
return HttpBadRequest({'code': 666, 'message':e.args[0]})
except ValidationError, e:
# Or do some JSON wrapping around the standard 500
return HttpBadRequest({'code': 777, 'message':', '.join(e.messages)})
except Exception, e:
# Rather than re-raising, we're going to things similar to
# what Django does. The difference is returning a serialized
# error message.
return self._handle_500(request, e)
return wrapper
You could overwrite tastypie's Resource method _handle_500(). The fact that it starts with an underscore indeed indicates that this is a "private" method and shouldn't be overwritten, but I find it a cleaner way than having to overwrite wrap_view() and replicate a lot of logic.
This is the way I use it:
from tastypie import http
from tastypie.resources import ModelResource
from tastypie.exceptions import TastypieError
class MyResource(ModelResource):
class Meta:
queryset = MyModel.objects.all()
fields = ('my', 'fields')
def _handle_500(self, request, exception):
if isinstance(exception, TastypieError):
data = {
'error_message': getattr(
settings,
'TASTYPIE_CANNED_ERROR',
'Sorry, this request could not be processed.'
),
}
return self.error_response(
request,
data,
response_class=http.HttpApplicationError
)
else:
return super(MyResource, self)._handle_500(request, exception)
In this case I catch all Tastypie errors by checking if exception is an instance of TastypieError and return a JSON reponse with the message "Sorry, this request could not be processed.". If it's a different exception, I call the parent _handle_500 using super(), which will create a django error page in development mode or send_admins() in production mode.
If you want to have a specific JSON response for a specific exception, just do the isinstance() check on a specific exception. Here are all the Tastypie exceptions:
https://github.com/toastdriven/django-tastypie/blob/master/tastypie/exceptions.py
Actually I think there should be a better/cleaner way to do this in Tastypie, so I opened a ticket on their github.