Django - redirect to login page vs. 403 - django

Why does Django redirects user to login page when trying to access pages protected by permissions? Wouldn't it make more sense to raise 403? Then I could display meaningful message in 403.html (using custom middleware) to user saying they don't have permissions to perform the action. Also I would be able to identify links to views that user shouldn't even be presented with at first place or users trying to access forbidden resources.

For future googlers, the permission_required decorator accepts an optional raise_exception keyword argument that will trigger a 403 if the user doesn't have the appropriate permission.

If you're talking about the login_required decorator, there's no reason you have to use that. You could write a similar decorator that did the very thing you're looking for (return a 403 response).
Unfortunately, the login_required decorator code is actually somewhat complex so it wouldn't be trivial to just copy/modify for your needs, as the redirect portion is actually within the user_passes_test function that they use.

Related

How to restrict anonymous users from GraphQL API endpoint?

Django has two approaches.
Regular DRF restricts user on Middleware level. So not logged in user doesn't reach anything.
GraphQL, on contrary, uses "per method" approach. So middleware passes all the request and each method. But afterward method calls decorator.
I want to implement 1st approach but for GraphQL. But in that case I need to open path for login mutation. How can I extract mutation name from payload?
If you want restrict a GraphQL API endpoint to Django logged in users, you can do it by extending GraphQLView with LoginRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
"""Adds a login requirement to graphQL API access via main endpoint."""
pass
and then adding this view to your urls.py like
path('api/', PrivateGraphQLView.as_view(schema=schema), name='api')
in the usual way as per the docs.
If you don't want to protect your entire API, you can create another schema and endpoint for the unprotected queries and mutations, which allows a clear separation between each. For example in urls.py:
path('public_api/', GraphQLView.as_view(schema=public_schema), name='public_api')
Note that every API endpoint must have at least one query to work or it will cause an assertion error.
Not sure if it serves your purpose, but I've used the following library which used JWT authentication with graphene similar to how JWT with DRF works!
https://github.com/flavors/django-graphql-jwt

Django: CSRF check only when certain conditions apply

I have a simple REST API that needs to be accessible both from a web app as well as from a remote service.
The remote service is authenticated via a custom HTTP header containing an API key.
How can I protect that API such that requests from a web browser are CSRF protected, but the CSRF check is not done when authenticated via API key? Or, in general, how can I enable CSRF protection for some requests on a specific view, but not others?
Currently, I have a decorator that checks the request for an API key and authenticates the API user roughly this way:
# Regular auth
if request.user.is_authenticated():
# DO CSRF verification, then continue calling the view
elif 'HTTP_X_API_KEY' in request.META:
api_key = request.META['HTTP_X_API_KEY']
user = authenticate(username=settings.API_USER_NAME, password=api_key)
login(request, user)
# If user is authenticated and autzorized, continue calling the view
# but WITHOUT invoking CSRF protection
The problem as stated is, that I only want CSRF protection for regular users, but not for the API user.
Okay, so after a bit more of tinkering around, the solution was to disable the CSRF middleware and enable csrf_protect for all cases where CSRF protection is required.
This works in the special case of the API, as every call is being decorated anyway, so the risk of some view being forgotten is negligible.
What does not work however, is the other way around, using csrf_exempt. The problem there is that the decorator only sets a csrf_exempt property on the view, which, if you have multiple decorators, may be masked again, even if you use functools.wraps.
Also, since csrf_exempt puts a property on the view, it cannot be dynamically enabled or disabled based on the request's content - it's really a very static thing to do.

Question about #login_required decorator and the redirect type

By default, when using the #login_required decorator, Django performs a 302 (temporary) redirect when redirecting a non-authenticated user to the login page. I work in conjunction with an SEO company (I know nothing of the topic myself) and he insists that the 301 (permanent) redirect is essential to the work that he is doing.
Is there anyway to force Django to perform a 301 redirect while using the #login_required decorator?
Thanks again.
The #login_required decorator uses the redirect_to_login view, which returns a Django HttpResponseRedirect object to redirect the user to the login page. This object represents, as you mention, a 302 redirect. There is an alternative redirect object, the HttpResponsePermanentRedirect, though you would need to write your own decorator which uses this instead.
Writing your own decorator is, of course, possible. It would be bad practice though, in my opinion. Not least because it ties your app to a particular implementation of the authentication module, but also because a 302 redirect is actually the correct one to use in this case.
The fact is that the page has not "moved permanently". Instead, the user simply needs to authenticate himself/herself before accessing the same URL once again. For this reason, the redirect is not a permanent one, as the page has not actually "moved".
login_required uses
user_passes_test, which in turn calls
redirect_to_login in which the
HttpResponseRedirect is hard-wired.
So no, the type of redirect cannot be changed just using login_required alone. You could write your own login_required decorator to provide 301 redirect (although the use of this here is disputable).

Django: Applying mutilple access control decorators to a view

I'm attempting to expose a single API call using three different authentication mechanisms: django's login_required , HTTP basic auth, and OAuth. I have decorators for all three but can't quite figure out how to have them all get along smoothly.
The required logic is to allow access to the view if any of the decorators / authentication mechanisms are valid for the user's request - basically an OR. However, if I simply include all three decorators then they all want to be satisfied before letting the request through - an AND.
What's a good way to deal with this?
I'm not sure you can. Suppose the user isn't logged in: if using login_required the server would redirect to a login form, whereas using basic auth, the server would return a 401 error page with a WWW-Authenticate response header. Which of these do you want to happen? I don't see how it could be both.

Django Middleware + URL's

Can a middleware check to see if a value is in the url, such as an image id ("/image/152/"), and if it is then do some checks to make sure the current user has permission to view that image and if not redirect to another url?
I had to roll my own permissions for this site I am working on and I don't want to clog up almost every view I write for the whole site with the same code, so I thought a middleware would be a good idea for this, but I'm not sure how to go about doing it.
Yes, this is possible. The django middleware docs for process_request indicate that:
def process_request(self, request)
request is an HttpRequest object. This method is called on each request, before Django decides which view to execute.
process_request() should return either None or an HttpResponse object. If it returns None, Django will continue processing this request, executing any other middleware and, then, the appropriate view. If it returns an HttpResponse object, Django won't bother calling ANY other request, view or exception middleware, or the appropriate view; it'll return that HttpResponse.
The HttpRequest object has a path attribute that will give you the URL that was requested.
If you prefer, however, note that you can also extend Django's system for authentication backends to populate the user in the request with permissions based on any arbitrary criteria, such as perhaps your hand-rolled permissions scheme. This way, you can leverage the default authentication decorators (#permission_required and #user_passes_test), and other apps/the admin site will be able to honour your permissions as well. The User object and permissions created do not need to reside in Django's user/permission tables, and can be created virtually on login; I've had a fair amount of success with this.
See Writing an authentication backend if this appeals.
If you implement authorization (Permission system) in middleware, you will end up with two options:
Check URL and allow to access
Check URL and reject access
If your requirement is that much simple, it is fine, since you don't have to touch views.
But in general, Permission system is much complex than that, for example:
User can access FOO/show_all/
But, he can't see or access foo instance, i.e, FOO/show/foo_1/
Since, he can't access foo_1 instance, we should not show them in show_all
(all foo instances)
If you want implement above 3 together, I suggest you write your own custom authorization backend for Django. All you need to do is implement few methods (your specific logic) and attach as backend.