summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-04-16 15:44:00 -0300
committernessita <124304+nessita@users.noreply.github.com>2025-04-17 12:00:20 -0300
commit8a0ad1ebe313a4f4fca6e9068c06ee400d15b8a4 (patch)
tree203a0b9d866457743f74d60d4df21f4bba8bda2e
parentd469db978ea6a705549b9519313d9adc198e4232 (diff)
Refs #35959 -- Added render_password_as_hash auth template tag for password rendering.
-rw-r--r--django/contrib/auth/forms.py21
-rw-r--r--django/contrib/auth/templates/auth/widgets/read_only_password_hash.html7
-rw-r--r--django/contrib/auth/templatetags/__init__.py0
-rw-r--r--django/contrib/auth/templatetags/auth.py25
-rw-r--r--tests/auth_tests/test_forms.py23
-rw-r--r--tests/auth_tests/test_templatetags.py37
6 files changed, 88 insertions, 25 deletions
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index cd02887027..7b0b3833b8 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -3,7 +3,7 @@ import unicodedata
from django import forms
from django.contrib.auth import authenticate, get_user_model, password_validation
-from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
+from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
@@ -13,7 +13,6 @@ from django.template import loader
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.text import capfirst
-from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_variables
@@ -40,24 +39,6 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
usable_password = value and not value.startswith(UNUSABLE_PASSWORD_PREFIX)
- summary = []
- if usable_password:
- try:
- hasher = identify_hasher(value)
- except ValueError:
- summary.append(
- {
- "label": gettext(
- "Invalid password format or unknown hashing algorithm."
- )
- }
- )
- else:
- for key, value_ in hasher.safe_summary(value).items():
- summary.append({"label": gettext(key), "value": value_})
- else:
- summary.append({"label": gettext("No password set.")})
- context["summary"] = summary
context["button_label"] = (
_("Reset password") if usable_password else _("Set password")
)
diff --git a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
index b48204b7eb..102ceb30eb 100644
--- a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
+++ b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
@@ -1,8 +1,5 @@
+{% load auth %}
<div{% include 'django/forms/widgets/attrs.html' %}>
- <p>
- {% for entry in summary %}
- <strong>{{ entry.label }}</strong>{% if entry.value %}: <bdi>{{ entry.value }}</bdi>{% endif %}
- {% endfor %}
- </p>
+ {% render_password_as_hash widget.value %}
<p><a role="button" class="button" href="{{ password_url|default:"../password/" }}">{{ button_label }}</a></p>
</div>
diff --git a/django/contrib/auth/templatetags/__init__.py b/django/contrib/auth/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/auth/templatetags/__init__.py
diff --git a/django/contrib/auth/templatetags/auth.py b/django/contrib/auth/templatetags/auth.py
new file mode 100644
index 0000000000..7cdbad8595
--- /dev/null
+++ b/django/contrib/auth/templatetags/auth.py
@@ -0,0 +1,25 @@
+from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
+from django.template import Library
+from django.utils.html import format_html, format_html_join
+from django.utils.translation import gettext
+
+register = Library()
+
+
+@register.simple_tag
+def render_password_as_hash(value):
+ if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
+ return format_html("<p><strong>{}</strong></p>", gettext("No password set."))
+ try:
+ hasher = identify_hasher(value)
+ hashed_summary = hasher.safe_summary(value)
+ except ValueError:
+ return format_html(
+ "<p><strong>{}</strong></p>",
+ gettext("Invalid password format or unknown hashing algorithm."),
+ )
+ items = [(gettext(key), val) for key, val in hashed_summary.items()]
+ return format_html(
+ "<p>{}</p>",
+ format_html_join(" ", "<strong>{}</strong>: <bdi>{}</bdi>", items),
+ )
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index 0f8b48286a..df91f100f5 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -1445,6 +1445,29 @@ class ReadOnlyPasswordHashTest(SimpleTestCase):
"</div>",
)
+ def test_render_no_password(self):
+ widget = ReadOnlyPasswordHashWidget()
+ self.assertHTMLEqual(
+ widget.render("name", None, {}),
+ "<div><p><strong>No password set.</p><p>"
+ '<a role="button" class="button" href="../password/">Set password</a>'
+ "</p></div>",
+ )
+
+ @override_settings(
+ PASSWORD_HASHERS=["django.contrib.auth.hashers.PBKDF2PasswordHasher"]
+ )
+ def test_render_invalid_password_format(self):
+ widget = ReadOnlyPasswordHashWidget()
+ value = "pbkdf2_sh"
+ self.assertHTMLEqual(
+ widget.render("name", value, {}),
+ "<div><p>"
+ "<strong>Invalid password format or unknown hashing algorithm.</strong>"
+ '</p><p><a role="button" class="button" href="../password/">Reset password'
+ "</a></p></div>",
+ )
+
def test_readonly_field_has_changed(self):
field = ReadOnlyPasswordHashField()
self.assertIs(field.disabled, True)
diff --git a/tests/auth_tests/test_templatetags.py b/tests/auth_tests/test_templatetags.py
new file mode 100644
index 0000000000..5cd5f558bd
--- /dev/null
+++ b/tests/auth_tests/test_templatetags.py
@@ -0,0 +1,37 @@
+from django.contrib.auth.hashers import make_password
+from django.contrib.auth.templatetags.auth import render_password_as_hash
+from django.test import SimpleTestCase, override_settings
+
+
+class RenderPasswordAsHashTests(SimpleTestCase):
+ @override_settings(
+ PASSWORD_HASHERS=["django.contrib.auth.hashers.PBKDF2PasswordHasher"]
+ )
+ def test_valid_password(self):
+ value = (
+ "pbkdf2_sha256$100000$a6Pucb1qSFcD$WmCkn9Hqidj48NVe5x0FEM6A9YiOqQcl/83m2Z5u"
+ "dm0="
+ )
+ hashed_html = (
+ "<p><strong>algorithm</strong>: <bdi>pbkdf2_sha256</bdi> "
+ "<strong>iterations</strong>: <bdi>100000</bdi> "
+ "<strong>salt</strong>: <bdi>a6Pucb******</bdi> "
+ "<strong>hash</strong>: <bdi>WmCkn9**************************************"
+ "</bdi></p>"
+ )
+ self.assertEqual(render_password_as_hash(value), hashed_html)
+
+ def test_invalid_password(self):
+ expected = (
+ "<p><strong>Invalid password format or unknown hashing algorithm.</strong>"
+ "</p>"
+ )
+ for value in ["pbkdf2_sh", "md5$password", "invalid", "testhash$password"]:
+ with self.subTest(value=value):
+ self.assertEqual(render_password_as_hash(value), expected)
+
+ def test_no_password(self):
+ expected = "<p><strong>No password set.</strong></p>"
+ for value in ["", None, make_password(None)]:
+ with self.subTest(value=value):
+ self.assertEqual(render_password_as_hash(value), expected)