summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2022-03-24 17:41:53 +0100
committerGitHub <noreply@github.com>2022-03-24 17:41:53 +0100
commit94d8ed55fa8e181b98f818a1b2805c66943cfeec (patch)
tree4da39a8ddb814c7bcc91feb4f6dde1174ebad549
parent1b695fbbc203c163bf7b8d78f6313a6c506fb938 (diff)
Refs #15619 -- Logged out with POST requests in admin.
-rw-r--r--django/contrib/admin/static/admin/css/base.css25
-rw-r--r--django/contrib/admin/templates/admin/base.html5
-rw-r--r--django/contrib/admin/templates/registration/password_change_done.html8
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html8
-rw-r--r--docs/releases/4.1.txt2
-rw-r--r--tests/admin_views/tests.py78
-rw-r--r--tests/auth_tests/test_views.py2
7 files changed, 84 insertions, 44 deletions
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index 910740e6ec..d62cdb99af 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -878,7 +878,7 @@ a.deletelink:focus, a.deletelink:hover {
overflow: hidden;
}
-#header a:link, #header a:visited {
+#header a:link, #header a:visited, #logout-form button {
color: var(--header-link-color);
}
@@ -914,22 +914,37 @@ a.deletelink:focus, a.deletelink:hover {
text-decoration: none;
}
+#logout-form {
+ display: inline;
+}
+
+#logout-form button {
+ background: none;
+ border: 0;
+ cursor: pointer;
+ font-family: "Roboto","Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif;
+}
+
#user-tools {
float: right;
- padding: 0;
margin: 0 0 0 20px;
+ text-align: right;
+}
+
+#user-tools, #logout-form button{
+ padding: 0;
font-weight: 300;
font-size: 0.6875rem;
letter-spacing: 0.5px;
text-transform: uppercase;
- text-align: right;
}
-#user-tools a {
+#user-tools a, #logout-form button {
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
}
-#user-tools a:focus, #user-tools a:hover {
+#user-tools a:focus, #user-tools a:hover,
+#logout-form button:active, #logout-form button:hover {
text-decoration: none;
border-bottom-color: var(--primary);
color: var(--primary);
diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
index 8ea3cd567c..b4bf2420ea 100644
--- a/django/contrib/admin/templates/admin/base.html
+++ b/django/contrib/admin/templates/admin/base.html
@@ -55,7 +55,10 @@
{% if user.has_usable_password %}
<a href="{% url 'admin:password_change' %}">{% translate 'Change password' %}</a> /
{% endif %}
- <a href="{% url 'admin:logout' %}">{% translate 'Log out' %}</a>
+ <form id="logout-form" method="post" action="{% url 'admin:logout' %}">
+ {% csrf_token %}
+ <button type="submit">{% translate 'Log out' %}</button>
+ </form>
{% endblock %}
</div>
{% endif %}
diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html
index e1bb982aba..cb017825f2 100644
--- a/django/contrib/admin/templates/registration/password_change_done.html
+++ b/django/contrib/admin/templates/registration/password_change_done.html
@@ -1,6 +1,12 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
-{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> / {% endif %}{% translate 'Change password' %} / <a href="{% url 'admin:logout' %}">{% translate 'Log out' %}</a>{% endblock %}
+{% block userlinks %}
+ {% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> / {% endif %}{% translate 'Change password' %} /
+ <form id="logout-form" method="post" action="{% url 'admin:logout' %}">
+ {% csrf_token %}
+ <button type="submit">{% translate 'Log out' %}</button>
+ </form>
+{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html
index c79020180b..8c2913398c 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -1,7 +1,13 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
-{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> / {% endif %} {% translate 'Change password' %} / <a href="{% url 'admin:logout' %}">{% translate 'Log out' %}</a>{% endblock %}
+{% block userlinks %}
+ {% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> / {% endif %} {% translate 'Change password' %} /
+ <form id="logout-form" method="post" action="{% url 'admin:logout' %}">
+ {% csrf_token %}
+ <button type="submit">{% translate 'Log out' %}</button>
+ </form>
+{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index b8dbc36da0..885c9329c5 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -436,6 +436,8 @@ Miscellaneous
* The ``size`` argument of the undocumented
``django.views.static.was_modified_since()`` function is removed.
+* The admin log out UI now uses ``POST`` requests.
+
.. _deprecated-features-4.1:
Features deprecated in 4.1
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 8c020cace7..669ad749d9 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -911,7 +911,11 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
def test_logout_and_password_change_URLs(self):
response = self.client.get(reverse("admin:admin_views_article_changelist"))
- self.assertContains(response, '<a href="%s">' % reverse("admin:logout"))
+ self.assertContains(
+ response,
+ '<form id="logout-form" method="post" action="%s">'
+ % reverse("admin:logout"),
+ )
self.assertContains(
response, '<a href="%s">' % reverse("admin:password_change")
)
@@ -1411,14 +1415,15 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
reverse("admin:app_list", args=("admin_views",)),
reverse("admin:admin_views_article_delete", args=(self.a1.pk,)),
reverse("admin:admin_views_article_history", args=(self.a1.pk,)),
- # Login must be after logout.
- reverse("admin:logout"),
- reverse("admin:login"),
]
for url in tests:
with self.subTest(url=url):
with self.assertNoLogs("django.template", "DEBUG"):
self.client.get(url)
+ # Login must be after logout.
+ with self.assertNoLogs("django.template", "DEBUG"):
+ self.client.post(reverse("admin:logout"))
+ self.client.get(reverse("admin:login"))
def test_render_delete_selected_confirmation_no_subtitle(self):
post_data = {
@@ -1833,7 +1838,7 @@ class CustomModelAdminTest(AdminViewBasicTestCase):
self.assertContains(response, "Hello from a custom login template")
def test_custom_admin_site_logout_template(self):
- response = self.client.get(reverse("admin2:logout"))
+ response = self.client.post(reverse("admin2:logout"))
self.assertIsInstance(response, TemplateResponse)
self.assertTemplateUsed(response, "custom_admin/logout.html")
self.assertContains(response, "Hello from a custom logout template")
@@ -2050,7 +2055,7 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.super_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Test if user enters email address
response = self.client.get(self.index_url)
@@ -2072,7 +2077,7 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.viewuser_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Add User
response = self.client.get(self.index_url)
@@ -2080,7 +2085,7 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.adduser_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Change User
response = self.client.get(self.index_url)
@@ -2088,7 +2093,7 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.changeuser_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Delete User
response = self.client.get(self.index_url)
@@ -2096,7 +2101,7 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.deleteuser_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Regular User should not be able to login.
response = self.client.get(self.index_url)
@@ -2137,7 +2142,7 @@ class AdminViewPermissionsTest(TestCase):
)
self.assertRedirects(login, reverse("has_permission_admin:index"))
self.assertFalse(login.context)
- self.client.get(reverse("has_permission_admin:logout"))
+ self.client.post(reverse("has_permission_admin:logout"))
# Staff should be able to login.
response = self.client.get(reverse("has_permission_admin:index"))
@@ -2152,7 +2157,7 @@ class AdminViewPermissionsTest(TestCase):
)
self.assertRedirects(login, reverse("has_permission_admin:index"))
self.assertFalse(login.context)
- self.client.get(reverse("has_permission_admin:logout"))
+ self.client.post(reverse("has_permission_admin:logout"))
def test_login_successfully_redirects_to_original_URL(self):
response = self.client.get(self.index_url)
@@ -2192,7 +2197,7 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.super_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
def test_login_page_notice_for_non_staff_users(self):
"""
@@ -2234,7 +2239,7 @@ class AdminViewPermissionsTest(TestCase):
post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
self.assertEqual(post.status_code, 403)
self.assertEqual(Article.objects.count(), 3)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# View User should not have access to add articles
self.client.force_login(self.viewuser)
@@ -2268,7 +2273,7 @@ class AdminViewPermissionsTest(TestCase):
'<li class="success">The article “Døm ikke” was added successfully.</li>',
)
article.delete()
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Add user may login and POST to add view, then redirect to admin root
self.client.force_login(self.adduser)
@@ -2289,7 +2294,7 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(Article.objects.count(), 4)
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].subject, "Greetings from a created object")
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# The addition was logged correctly
addition_log = LogEntry.objects.all()[0]
@@ -2316,7 +2321,7 @@ class AdminViewPermissionsTest(TestCase):
post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
self.assertRedirects(post, reverse("admin:admin_views_article_changelist"))
self.assertEqual(Article.objects.count(), 5)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# 8509 - if a normal user is already logged in, it is possible
# to change user into the superuser without error
@@ -2371,7 +2376,7 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(response.status_code, 403)
post = self.client.post(article_change_url, change_dict)
self.assertEqual(post.status_code, 403)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# view user can view articles but not make changes.
self.client.force_login(self.viewuser)
@@ -2397,7 +2402,7 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(
Article.objects.get(pk=self.a1.pk).content, "<p>Middle content</p>"
)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# change user can view all items and edit them
self.client.force_login(self.changeuser)
@@ -2443,7 +2448,7 @@ class AdminViewPermissionsTest(TestCase):
"errors"
),
)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# Test redirection when using row-level change permissions. Refs #11513.
r1 = RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
@@ -2502,7 +2507,7 @@ class AdminViewPermissionsTest(TestCase):
)
self.assertRedirects(response, self.index_url)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
for login_user in [self.joepublicuser, self.nostaffuser]:
with self.subTest(login_user.username):
@@ -2525,7 +2530,7 @@ class AdminViewPermissionsTest(TestCase):
RowLevelChangePermissionModel.objects.get(id=2).name, "changed"
)
self.assertContains(response, "login-form")
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
def test_change_view_without_object_change_permission(self):
"""
@@ -2785,7 +2790,7 @@ class AdminViewPermissionsTest(TestCase):
reverse("admin:admin_views_article_history", args=(self.a1.pk,))
)
self.assertEqual(response.status_code, 403)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# view user can view all items
self.client.force_login(self.viewuser)
@@ -2793,7 +2798,7 @@ class AdminViewPermissionsTest(TestCase):
reverse("admin:admin_views_article_history", args=(self.a1.pk,))
)
self.assertEqual(response.status_code, 200)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
# change user can view all items and edit them
self.client.force_login(self.changeuser)
@@ -2829,7 +2834,7 @@ class AdminViewPermissionsTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
for login_user in [self.joepublicuser, self.nostaffuser]:
with self.subTest(login_user.username):
@@ -2847,7 +2852,7 @@ class AdminViewPermissionsTest(TestCase):
response = self.client.get(url, follow=True)
self.assertContains(response, "login-form")
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
def test_history_view_bad_url(self):
self.client.force_login(self.changeuser)
@@ -3229,7 +3234,7 @@ class AdminViewsNoUrlTest(TestCase):
r = self.client.get(reverse("admin:index"))
# we shouldn't get a 500 error caused by a NoReverseMatch
self.assertEqual(r.status_code, 200)
- self.client.get(reverse("admin:logout"))
+ self.client.post(reverse("admin:logout"))
@skipUnlessDBFeature("can_defer_constraint_checks")
@@ -3933,10 +3938,10 @@ class AdminViewListEditable(TestCase):
# 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
# main form submit button = 1
# search field and search submit button = 2
- # CSRF field = 1
+ # CSRF field = 2
# field to track 'select all' across paginated views = 1
- # 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
- self.assertContains(response, "<input", count=20)
+ # 6 + 4 + 4 + 1 + 2 + 2 + 1 = 20 inputs
+ self.assertContains(response, "<input", count=21)
# 1 select per object = 3 selects
self.assertContains(response, "<select", count=4)
@@ -4513,6 +4518,7 @@ class AdminInheritedInlinesTest(TestCase):
# test the add case
response = self.client.get(reverse("admin:admin_views_persona_add"))
names = name_re.findall(response.content)
+ names.remove(b"csrfmiddlewaretoken")
# make sure we have no duplicate HTML names
self.assertEqual(len(names), len(set(names)))
@@ -4549,6 +4555,7 @@ class AdminInheritedInlinesTest(TestCase):
reverse("admin:admin_views_persona_change", args=(persona_id,))
)
names = name_re.findall(response.content)
+ names.remove(b"csrfmiddlewaretoken")
# make sure we have no duplicate HTML names
self.assertEqual(len(names), len(set(names)))
@@ -5366,7 +5373,7 @@ class NeverCacheTests(TestCase):
def test_logout(self):
"Check the never-cache status of logout view"
- response = self.client.get(reverse("admin:logout"))
+ response = self.client.post(reverse("admin:logout"))
self.assertEqual(get_max_age(response), 0)
def test_password_change(self):
@@ -6221,7 +6228,8 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
self.assertNotContains(response, 'name="posted"')
# 3 fields + 2 submit buttons + 5 inline management form fields, + 2
# hidden fields for inlines + 1 field for the inline + 2 empty form
- self.assertContains(response, "<input", count=16)
+ # + 1 logout form.
+ self.assertContains(response, "<input", count=17)
self.assertContains(response, formats.localize(datetime.date.today()))
self.assertContains(response, "<label>Awesomeness level:</label>")
self.assertContains(response, "Very awesome.")
@@ -7335,7 +7343,7 @@ class AdminViewLogoutTests(TestCase):
def test_logout(self):
self.client.force_login(self.superuser)
- response = self.client.get(reverse("admin:logout"))
+ response = self.client.post(reverse("admin:logout"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "registration/logged_out.html")
self.assertEqual(response.request["PATH_INFO"], reverse("admin:logout"))
@@ -7345,13 +7353,13 @@ class AdminViewLogoutTests(TestCase):
) # user-tools div shouldn't visible.
def test_client_logout_url_can_be_used_to_login(self):
- response = self.client.get(reverse("admin:logout"))
+ response = self.client.post(reverse("admin:logout"))
self.assertEqual(
response.status_code, 302
) # we should be redirected to the login page.
# follow the redirect and test results.
- response = self.client.get(reverse("admin:logout"), follow=True)
+ response = self.client.post(reverse("admin:logout"), follow=True)
self.assertContains(
response,
'<input type="hidden" name="next" value="%s">' % reverse("admin:index"),
diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
index 38e2234416..e056d0f002 100644
--- a/tests/auth_tests/test_views.py
+++ b/tests/auth_tests/test_views.py
@@ -71,7 +71,7 @@ class AuthViewsTestCase(TestCase):
return response
def logout(self):
- response = self.client.get("/admin/logout/")
+ response = self.client.post("/admin/logout/")
self.assertEqual(response.status_code, 200)
self.assertNotIn(SESSION_KEY, self.client.session)