mirror of
https://github.com/django/django.git
synced 2025-10-31 09:41:08 +00:00
Fixed #31405 -- Added LoginRequiredMiddleware.
Co-authored-by: Adam Johnson <me@adamj.eu> Co-authored-by: Mehmet İnce <mehmet@mehmetince.net> Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
This commit is contained in:
committed by
Sarah Boyce
parent
7857507c7f
commit
c7fc9f20b4
@@ -1,5 +1,14 @@
|
||||
from django.contrib.auth.checks import check_models_permissions, check_user_model
|
||||
from django.contrib.auth.checks import (
|
||||
check_middleware,
|
||||
check_models_permissions,
|
||||
check_user_model,
|
||||
)
|
||||
from django.contrib.auth.middleware import (
|
||||
AuthenticationMiddleware,
|
||||
LoginRequiredMiddleware,
|
||||
)
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
from django.db.models import Q, UniqueConstraint
|
||||
@@ -345,3 +354,102 @@ class ModelsPermissionsChecksTests(SimpleTestCase):
|
||||
default_permissions = ()
|
||||
|
||||
self.assertEqual(checks.run_checks(self.apps.get_app_configs()), [])
|
||||
|
||||
|
||||
class LoginRequiredMiddlewareSubclass(LoginRequiredMiddleware):
|
||||
redirect_field_name = "redirect_to"
|
||||
|
||||
|
||||
class AuthenticationMiddlewareSubclass(AuthenticationMiddleware):
|
||||
pass
|
||||
|
||||
|
||||
class SessionMiddlewareSubclass(SessionMiddleware):
|
||||
pass
|
||||
|
||||
|
||||
@override_system_checks([check_middleware])
|
||||
class MiddlewareChecksTests(SimpleTestCase):
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"auth_tests.test_checks.SessionMiddlewareSubclass",
|
||||
"auth_tests.test_checks.AuthenticationMiddlewareSubclass",
|
||||
"auth_tests.test_checks.LoginRequiredMiddlewareSubclass",
|
||||
]
|
||||
)
|
||||
def test_middleware_subclasses(self):
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"auth_tests.test_checks",
|
||||
"auth_tests.test_checks.NotExist",
|
||||
]
|
||||
)
|
||||
def test_invalid_middleware_skipped(self):
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"django.contrib.does.not.Exist",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.auth.middleware.LoginRequiredMiddleware",
|
||||
]
|
||||
)
|
||||
def test_check_ignores_import_error_in_middleware(self):
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.auth.middleware.LoginRequiredMiddleware",
|
||||
]
|
||||
)
|
||||
def test_correct_order_with_login_required_middleware(self):
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"django.contrib.auth.middleware.LoginRequiredMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
]
|
||||
)
|
||||
def test_incorrect_order_with_login_required_middleware(self):
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(
|
||||
errors,
|
||||
[
|
||||
checks.Error(
|
||||
"In order to use django.contrib.auth.middleware."
|
||||
"LoginRequiredMiddleware, django.contrib.auth.middleware."
|
||||
"AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
|
||||
id="auth.E013",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"django.contrib.auth.middleware.LoginRequiredMiddleware",
|
||||
]
|
||||
)
|
||||
def test_missing_authentication_with_login_required_middleware(self):
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(
|
||||
errors,
|
||||
[
|
||||
checks.Error(
|
||||
"In order to use django.contrib.auth.middleware."
|
||||
"LoginRequiredMiddleware, django.contrib.auth.middleware."
|
||||
"AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
|
||||
id="auth.E013",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import models
|
||||
from django.contrib.auth.decorators import (
|
||||
login_not_required,
|
||||
login_required,
|
||||
permission_required,
|
||||
user_passes_test,
|
||||
@@ -113,6 +114,40 @@ class LoginRequiredTestCase(AuthViewsTestCase):
|
||||
await self.test_login_required_async_view(login_url="/somewhere/")
|
||||
|
||||
|
||||
class LoginNotRequiredTestCase(TestCase):
|
||||
"""
|
||||
Tests the login_not_required decorators
|
||||
"""
|
||||
|
||||
def test_callable(self):
|
||||
"""
|
||||
login_not_required is assignable to callable objects.
|
||||
"""
|
||||
|
||||
class CallableView:
|
||||
def __call__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
login_not_required(CallableView())
|
||||
|
||||
def test_view(self):
|
||||
"""
|
||||
login_not_required is assignable to normal views.
|
||||
"""
|
||||
|
||||
def normal_view(request):
|
||||
pass
|
||||
|
||||
login_not_required(normal_view)
|
||||
|
||||
def test_decorator_marks_view_as_login_not_required(self):
|
||||
@login_not_required
|
||||
def view(request):
|
||||
return HttpResponse()
|
||||
|
||||
self.assertFalse(view.login_required)
|
||||
|
||||
|
||||
class PermissionsRequiredDecoratorTest(TestCase):
|
||||
"""
|
||||
Tests for the permission_required decorator
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.middleware import (
|
||||
AuthenticationMiddleware,
|
||||
LoginRequiredMiddleware,
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, modify_settings, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestAuthenticationMiddleware(TestCase):
|
||||
@@ -50,3 +56,134 @@ class TestAuthenticationMiddleware(TestCase):
|
||||
self.assertEqual(auser, self.user)
|
||||
auser_second = await self.request.auser()
|
||||
self.assertIs(auser, auser_second)
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF="auth_tests.urls")
|
||||
@modify_settings(
|
||||
MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
|
||||
)
|
||||
class TestLoginRequiredMiddleware(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
"test_user", "test@example.com", "test_password"
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = LoginRequiredMiddleware(lambda req: HttpResponse())
|
||||
self.request = HttpRequest()
|
||||
|
||||
def test_public_paths(self):
|
||||
paths = ["public_view", "public_function_view"]
|
||||
for path in paths:
|
||||
response = self.client.get(f"/{path}/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_protected_paths(self):
|
||||
paths = ["protected_view", "protected_function_view"]
|
||||
for path in paths:
|
||||
response = self.client.get(f"/{path}/")
|
||||
self.assertRedirects(
|
||||
response,
|
||||
settings.LOGIN_URL + f"?next=/{path}/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
def test_login_required_paths(self):
|
||||
paths = ["login_required_cbv_view", "login_required_decorator_view"]
|
||||
for path in paths:
|
||||
response = self.client.get(f"/{path}/")
|
||||
self.assertRedirects(
|
||||
response,
|
||||
"/custom_login/" + f"?step=/{path}/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
def test_admin_path(self):
|
||||
admin_url = reverse("admin:index")
|
||||
response = self.client.get(admin_url)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse("admin:login") + f"?next={admin_url}",
|
||||
target_status_code=200,
|
||||
)
|
||||
|
||||
def test_non_existent_path(self):
|
||||
response = self.client.get("/non_existent/")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_paths_with_logged_in_user(self):
|
||||
paths = [
|
||||
"public_view",
|
||||
"public_function_view",
|
||||
"protected_view",
|
||||
"protected_function_view",
|
||||
"login_required_cbv_view",
|
||||
"login_required_decorator_view",
|
||||
]
|
||||
self.client.login(username="test_user", password="test_password")
|
||||
for path in paths:
|
||||
response = self.client.get(f"/{path}/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_get_login_url_from_view_func(self):
|
||||
def view_func(request):
|
||||
return HttpResponse()
|
||||
|
||||
view_func.login_url = "/custom_login/"
|
||||
login_url = self.middleware.get_login_url(view_func)
|
||||
self.assertEqual(login_url, "/custom_login/")
|
||||
|
||||
@override_settings(LOGIN_URL="/settings_login/")
|
||||
def test_get_login_url_from_settings(self):
|
||||
login_url = self.middleware.get_login_url(lambda: None)
|
||||
self.assertEqual(login_url, "/settings_login/")
|
||||
|
||||
@override_settings(LOGIN_URL=None)
|
||||
def test_get_login_url_no_login_url(self):
|
||||
with self.assertRaises(ImproperlyConfigured) as e:
|
||||
self.middleware.get_login_url(lambda: None)
|
||||
self.assertEqual(
|
||||
str(e.exception),
|
||||
"No login URL to redirect to. Define settings.LOGIN_URL or provide "
|
||||
"a login_url via the 'django.contrib.auth.decorators.login_required' "
|
||||
"decorator.",
|
||||
)
|
||||
|
||||
def test_get_redirect_field_name_from_view_func(self):
|
||||
def view_func(request):
|
||||
return HttpResponse()
|
||||
|
||||
view_func.redirect_field_name = "next_page"
|
||||
redirect_field_name = self.middleware.get_redirect_field_name(view_func)
|
||||
self.assertEqual(redirect_field_name, "next_page")
|
||||
|
||||
@override_settings(
|
||||
MIDDLEWARE=[
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"auth_tests.test_checks.LoginRequiredMiddlewareSubclass",
|
||||
],
|
||||
LOGIN_URL="/settings_login/",
|
||||
)
|
||||
def test_login_url_resolve_logic(self):
|
||||
paths = ["login_required_cbv_view", "login_required_decorator_view"]
|
||||
for path in paths:
|
||||
response = self.client.get(f"/{path}/")
|
||||
self.assertRedirects(
|
||||
response,
|
||||
"/custom_login/" + f"?step=/{path}/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
paths = ["protected_view", "protected_function_view"]
|
||||
for path in paths:
|
||||
response = self.client.get(f"/{path}/")
|
||||
self.assertRedirects(
|
||||
response,
|
||||
f"/settings_login/?redirect_to=/{path}/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
def test_get_redirect_field_name_default(self):
|
||||
redirect_field_name = self.middleware.get_redirect_field_name(lambda: None)
|
||||
self.assertEqual(redirect_field_name, REDIRECT_FIELD_NAME)
|
||||
|
||||
@@ -32,7 +32,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import connection
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.middleware.csrf import CsrfViewMiddleware, get_token
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.test import Client, TestCase, modify_settings, override_settings
|
||||
from django.test.client import RedirectCycleError
|
||||
from django.urls import NoReverseMatch, reverse, reverse_lazy
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
@@ -472,6 +472,29 @@ class PasswordResetTest(AuthViewsTestCase):
|
||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
||||
self.client.get("/reset/missing_parameters/")
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
|
||||
)
|
||||
def test_access_under_login_required_middleware(self):
|
||||
reset_urls = [
|
||||
reverse("password_reset"),
|
||||
reverse("password_reset_done"),
|
||||
reverse("password_reset_confirm", kwargs={"uidb64": "abc", "token": "def"}),
|
||||
reverse("password_reset_complete"),
|
||||
]
|
||||
|
||||
for url in reset_urls:
|
||||
with self.subTest(url=url):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
"/password_reset/", {"email": "staffmember@example.com"}
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, "/password_reset/done/", fetch_redirect_response=False
|
||||
)
|
||||
|
||||
|
||||
@override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
|
||||
class CustomUserPasswordResetTest(AuthViewsTestCase):
|
||||
@@ -661,6 +684,38 @@ class ChangePasswordTest(AuthViewsTestCase):
|
||||
response, "/password_reset/", fetch_redirect_response=False
|
||||
)
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
|
||||
)
|
||||
def test_access_under_login_required_middleware(self):
|
||||
response = self.client.post(
|
||||
"/password_change/",
|
||||
{
|
||||
"old_password": "password",
|
||||
"new_password1": "password1",
|
||||
"new_password2": "password1",
|
||||
},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
settings.LOGIN_URL + "?next=/password_change/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
self.login()
|
||||
|
||||
response = self.client.post(
|
||||
"/password_change/",
|
||||
{
|
||||
"old_password": "password",
|
||||
"new_password1": "password1",
|
||||
"new_password2": "password1",
|
||||
},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, "/password_change/done/", fetch_redirect_response=False
|
||||
)
|
||||
|
||||
|
||||
class SessionAuthenticationTests(AuthViewsTestCase):
|
||||
def test_user_password_change_updates_session(self):
|
||||
@@ -904,6 +959,13 @@ class LoginTest(AuthViewsTestCase):
|
||||
response = self.login(url="/login/get_default_redirect_url/?next=/test/")
|
||||
self.assertRedirects(response, "/test/", fetch_redirect_response=False)
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
|
||||
)
|
||||
def test_access_under_login_required_middleware(self):
|
||||
response = self.client.get(reverse("login"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class LoginURLSettings(AuthViewsTestCase):
|
||||
"""Tests for settings.LOGIN_URL."""
|
||||
@@ -1355,6 +1417,22 @@ class LogoutTest(AuthViewsTestCase):
|
||||
self.assertContains(response, "Logged out")
|
||||
self.confirm_logged_out()
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
|
||||
)
|
||||
def test_access_under_login_required_middleware(self):
|
||||
response = self.client.post("/logout/")
|
||||
self.assertRedirects(
|
||||
response,
|
||||
settings.LOGIN_URL + "?next=/logout/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
self.login()
|
||||
|
||||
response = self.client.post("/logout/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
def get_perm(Model, perm):
|
||||
ct = ContentType.objects.get_for_model(Model)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.auth.decorators import (
|
||||
login_not_required,
|
||||
login_required,
|
||||
permission_required,
|
||||
)
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.urls import urlpatterns as auth_urlpatterns
|
||||
from django.contrib.auth.views import LoginView
|
||||
@@ -9,6 +13,8 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.template import RequestContext, Template
|
||||
from django.urls import path, re_path, reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.i18n import set_language
|
||||
|
||||
@@ -88,6 +94,42 @@ class CustomDefaultRedirectURLLoginView(LoginView):
|
||||
return "/custom/"
|
||||
|
||||
|
||||
class EmptyResponseBaseView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@method_decorator(login_not_required, name="dispatch")
|
||||
class PublicView(EmptyResponseBaseView):
|
||||
pass
|
||||
|
||||
|
||||
class ProtectedView(EmptyResponseBaseView):
|
||||
pass
|
||||
|
||||
|
||||
@method_decorator(
|
||||
login_required(login_url="/custom_login/", redirect_field_name="step"),
|
||||
name="dispatch",
|
||||
)
|
||||
class ProtectedViewWithCustomLoginRequired(EmptyResponseBaseView):
|
||||
pass
|
||||
|
||||
|
||||
@login_not_required
|
||||
def public_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
def protected_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@login_required(login_url="/custom_login/", redirect_field_name="step")
|
||||
def protected_view_with_login_required_decorator(request):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
# special urls for auth test cases
|
||||
urlpatterns = auth_urlpatterns + [
|
||||
path(
|
||||
@@ -198,7 +240,14 @@ urlpatterns = auth_urlpatterns + [
|
||||
"login_and_permission_required_exception/",
|
||||
login_and_permission_required_exception,
|
||||
),
|
||||
path("public_view/", PublicView.as_view()),
|
||||
path("public_function_view/", public_view),
|
||||
path("protected_view/", ProtectedView.as_view()),
|
||||
path("protected_function_view/", protected_view),
|
||||
path(
|
||||
"login_required_decorator_view/", protected_view_with_login_required_decorator
|
||||
),
|
||||
path("login_required_cbv_view/", ProtectedViewWithCustomLoginRequired.as_view()),
|
||||
path("setlang/", set_language, name="set_language"),
|
||||
# This line is only required to render the password reset with is_admin=True
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user