Skip to content

Commit

Permalink
Added LoginRequiredMiddleware
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Johnson <[email protected]>
Co-authored-by: Mehmet İnce <[email protected]>
Co-authored-by: Sarah Boyce <[email protected]>
  • Loading branch information
4 people committed May 5, 2024
1 parent 9a27c76 commit a369114
Show file tree
Hide file tree
Showing 13 changed files with 455 additions and 13 deletions.
7 changes: 6 additions & 1 deletion django/contrib/admin/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
from django.contrib.admin.exceptions import AlreadyRegistered, NotRegistered
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_not_required
from django.core.exceptions import ImproperlyConfigured
from django.db.models.base import ModelBase
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import NoReverseMatch, Resolver404, resolve, reverse
from django.urls import NoReverseMatch, Resolver404, resolve, reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.functional import LazyObject
from django.utils.module_loading import import_string
Expand Down Expand Up @@ -259,6 +260,9 @@ def wrapper(*args, **kwargs):
return self.admin_view(view, cacheable)(*args, **kwargs)

wrapper.admin_site = self
# login_url attribute is used by LoginRequiredMiddleware so it
# redirects to admin login.
wrapper.login_url = reverse_lazy("admin:login", current_app=self.name)
return update_wrapper(wrapper, view)

# Admin-site-wide views.
Expand Down Expand Up @@ -402,6 +406,7 @@ def logout(self, request, extra_context=None):
return LogoutView.as_view(**defaults)(request)

@method_decorator(never_cache)
@login_not_required
def login(self, request, extra_context=None):
"""
Display the login form for the given HttpRequest.
Expand Down
19 changes: 19 additions & 0 deletions django/contrib/auth/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ async def _view_wrapper(request, *args, **kwargs):
return await view_func(request, *args, **kwargs)
return _redirect_to_login(request)

# login_url and redirect_field_name attributes are used by
# LoginRequiredMiddleware so it redirects accordingly.
_view_wrapper.login_url = login_url
_view_wrapper.redirect_field_name = redirect_field_name

else:

def _view_wrapper(request, *args, **kwargs):
Expand All @@ -60,6 +65,11 @@ def _view_wrapper(request, *args, **kwargs):
return view_func(request, *args, **kwargs)
return _redirect_to_login(request)

# login_url and redirect_field_name attributes are used by
# LoginRequiredMiddleware so it redirects accordingly.
_view_wrapper.login_url = login_url
_view_wrapper.redirect_field_name = redirect_field_name

return wraps(view_func)(_view_wrapper)

return decorator
Expand All @@ -82,6 +92,15 @@ def login_required(
return actual_decorator


def login_not_required(view_func):
"""
Decorator for views that marks that the view is accessible by
unauthenticated users.
"""
view_func.login_required = False
return view_func


def permission_required(perm, login_url=None, raise_exception=False):
"""
Decorator for views that checks whether a user has a particular permission
Expand Down
102 changes: 100 additions & 2 deletions django/contrib/auth/middleware.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from functools import partial
from urllib.parse import urlparse

from django.conf import settings
from django.contrib import auth
from django.contrib.auth import load_backend
from django.contrib.auth import REDIRECT_FIELD_NAME, load_backend
from django.contrib.auth.backends import RemoteUserBackend
from django.core.exceptions import ImproperlyConfigured
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.shortcuts import resolve_url
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject

Expand Down Expand Up @@ -34,6 +38,100 @@ def process_request(self, request):
request.auser = partial(auser, request)


class LoginRequiredMiddleware(MiddlewareMixin):
"""
Middleware that forces all views to require login by default.
Views that have login_not_required decorator or LoginNotRequiredMixin mixin
will be able to pass through without this validation. Otherwise, it will
redirect user to login page or a custom login page if the view has
`login_url` or `redirect_field_name` attributes defined.
"""

login_url = None
permission_denied_message = ""
raise_exception = False
redirect_field_name = REDIRECT_FIELD_NAME

def process_request(self, request):
if not hasattr(request, "user"):
raise ImproperlyConfigured(
"The Django login required middleware requires authentication "
"middleware to be installed. Edit your MIDDLEWARE setting to "
"insert "
"'django.contrib.auth.middleware.AuthenticationMiddleware' "
"before "
"'django.contrib.auth.middleware.LoginRequiredMiddleware'."
)

def process_view(self, request, view_func, view_args, view_kwargs):
if request.user.is_authenticated:
return None

view_class = getattr(view_func, "view_class", None)
if view_class and not getattr(view_class, "login_required", True):
return None

if not getattr(view_func, "login_required", True):
return None

return self.handle_no_permission(request, view_func)

def get_login_url(self, view_func):
"""
Override this method to override the login_url attribute.
"""
login_url = self.login_url or getattr(view_func, "login_url", None)
view_class = getattr(view_func, "view_class", None)
if view_class:
login_url = getattr(view_class, "login_url", None)
login_url = login_url or settings.LOGIN_URL
if not login_url:
raise ImproperlyConfigured(
f"{self.__class__.__name__} is missing the login_url attribute. Define "
f"{self.__class__.__name__}.login_url, settings.LOGIN_URL, or override "
f"{self.__class__.__name__}.get_login_url()."
)
return str(login_url)

def get_permission_denied_message(self):
"""
Override this method to override the permission_denied_message attribute.
"""
return self.permission_denied_message

def get_redirect_field_name(self, view_func):
"""
Override this method to override the redirect_field_name attribute.
"""
redirect_field_name = getattr(view_func, "redirect_field_name", None)
view_class = getattr(view_func, "view_class", None)
if view_class:
redirect_field_name = getattr(view_class, "redirect_field_name", None)
return redirect_field_name or self.redirect_field_name

def handle_no_permission(self, request, view_func):
if self.raise_exception or request.user.is_authenticated:
raise PermissionDenied(self.get_permission_denied_message())

path = request.build_absolute_uri()
resolved_login_url = resolve_url(self.get_login_url(view_func))
# If the login url is the same scheme and net location then use the
# path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if (not login_scheme or login_scheme == current_scheme) and (
not login_netloc or login_netloc == current_netloc
):
path = request.get_full_path()

return redirect_to_login(
path,
resolved_login_url,
self.get_redirect_field_name(view_func),
)


class RemoteUserMiddleware(MiddlewareMixin):
"""
Middleware for utilizing web-server-provided authentication.
Expand Down
11 changes: 10 additions & 1 deletion django/contrib/auth/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.shortcuts import resolve_url

Expand Down Expand Up @@ -57,6 +56,8 @@ def handle_no_permission(self):
not login_netloc or login_netloc == current_netloc
):
path = self.request.get_full_path()
from django.contrib.auth.views import redirect_to_login

return redirect_to_login(
path,
resolved_login_url,
Expand All @@ -73,6 +74,14 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)


class LoginNotRequiredMixin:
"""
Mark the view as accessible by unauthenticated users.
"""

login_required = False


class PermissionRequiredMixin(AccessMixin):
"""Verify that the current user has all specified permissions."""

Expand Down
13 changes: 8 additions & 5 deletions django/contrib/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
PasswordResetForm,
SetPasswordForm,
)
from django.contrib.auth.mixins import LoginNotRequiredMixin
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ImproperlyConfigured, ValidationError
Expand Down Expand Up @@ -62,7 +63,7 @@ def get_default_redirect_url(self):
raise ImproperlyConfigured("No URL to redirect to. Provide a next_page.")


class LoginView(RedirectURLMixin, FormView):
class LoginView(RedirectURLMixin, LoginNotRequiredMixin, FormView):
"""
Display the login form and handle the login action.
"""
Expand Down Expand Up @@ -210,7 +211,7 @@ def get_context_data(self, **kwargs):
return context


class PasswordResetView(PasswordContextMixin, FormView):
class PasswordResetView(PasswordContextMixin, LoginNotRequiredMixin, FormView):
email_template_name = "registration/password_reset_email.html"
extra_email_context = None
form_class = PasswordResetForm
Expand Down Expand Up @@ -244,12 +245,12 @@ def form_valid(self, form):
INTERNAL_RESET_SESSION_TOKEN = "_password_reset_token"


class PasswordResetDoneView(PasswordContextMixin, TemplateView):
class PasswordResetDoneView(PasswordContextMixin, LoginNotRequiredMixin, TemplateView):
template_name = "registration/password_reset_done.html"
title = _("Password reset sent")


class PasswordResetConfirmView(PasswordContextMixin, FormView):
class PasswordResetConfirmView(PasswordContextMixin, LoginNotRequiredMixin, FormView):
form_class = SetPasswordForm
post_reset_login = False
post_reset_login_backend = None
Expand Down Expand Up @@ -335,7 +336,9 @@ def get_context_data(self, **kwargs):
return context


class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
class PasswordResetCompleteView(
PasswordContextMixin, LoginNotRequiredMixin, TemplateView
):
template_name = "registration/password_reset_complete.html"
title = _("Password reset complete")

Expand Down
30 changes: 30 additions & 0 deletions docs/ref/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,30 @@ Adds the ``user`` attribute, representing the currently-logged-in user, to
every incoming ``HttpRequest`` object. See :ref:`Authentication in web requests
<auth-web-requests>`.

.. class:: LoginRequiredMiddleware

.. versionadded:: 5.1

Redirects all requests by non-authenticated users to a login page. For admin
views, this will redirect to the admin login. For all other views, this will
redirect to :setting:`settings.LOGIN_URL <LOGIN_URL>`. This can be customized
by setting ``login_url`` or ``redirect_field_name`` attributes on your view.

Here are examples of how you can apply this in both class based and function
based views::

class MyView(View):
login_url = "/login/"
redirect_field_name = "redirect_to"


@login_required(redirect_field_name="redirect_to", login_url="/login/")
def my_view(request): ...

Views using the :func:`~django.contrib.auth.decorators.login_not_required`
decorator or the :class:`~django.contrib.auth.mixins.LoginNotRequiredMixin`
mixin are exempt from this requirement.

.. class:: RemoteUserMiddleware

Middleware for utilizing web server provided authentication. See
Expand Down Expand Up @@ -597,6 +621,12 @@ Here are some hints about the ordering of various Django middleware classes:

After ``SessionMiddleware``: uses session storage.

#. :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware`

.. versionadded:: 5.1

After ``AuthenticationMiddleware``: uses user object.

#. :class:`~django.contrib.messages.middleware.MessageMiddleware`

After ``SessionMiddleware``: can use session-based storage.
Expand Down
6 changes: 6 additions & 0 deletions docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ Minor features
accessibility of the
:class:`~django.contrib.auth.forms.UserChangeForm`.

* The :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware` is now
available. It forces all views to require login by default. The
:class:`~django.contrib.auth.mixins.LoginNotRequiredMixin` mixin and
:func:`~django.contrib.auth.decorators.login_not_required` decorator have
also been added.

:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
42 changes: 42 additions & 0 deletions docs/topics/auth/default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,48 @@ inheritance list.

.. currentmodule:: django.contrib.auth.decorators

The ``login_not_required`` decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 5.1

.. function:: login_not_required()

The ``login_not_required`` decorator can be used with the
``LoginRequiredMiddleware``::

from django.contrib.auth.decorators import login_not_required


@login_not_required
def my_view(request): ...

This decorator indicates that the view is accessible to non-authenticated
users.

.. currentmodule:: django.contrib.auth.mixins

The ``LoginNotRequiredMixin`` mixin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 5.1

When using :doc:`class-based views </topics/class-based-views/index>`, the
``LoginNotRequiredMixin`` can be used to achieve the same behavior as with the
``login_not_required`` decorator.

.. class:: LoginNotRequiredMixin

If a view uses this mixin, all requests by non-authenticated users will be
allowed to pass without any redirection to login::

from django.contrib.auth.mixins import LoginNotRequiredMixin


class MyView(LoginNotRequiredMixin, View): ...

.. currentmodule:: django.contrib.auth.decorators

Limiting access to logged-in users that pass a test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down

0 comments on commit a369114

Please sign in to comment.