From 73f9192cd168b00ffde9dc2af0ccba09cde3a012 Mon Sep 17 00:00:00 2001 From: Michael Cooper Date: Wed, 16 May 2018 21:12:59 +1000 Subject: [PATCH] Allow user to set the message in Django settings. --- docs/validator.rst | 36 +++++++++++++ .../tests/test_validators.py | 52 ++++++++++++++++++- pwned_passwords_django/validators.py | 32 +++++++++--- 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/docs/validator.rst b/docs/validator.rst index f9ac38e..cc28fa7 100644 --- a/docs/validator.rst +++ b/docs/validator.rst @@ -46,3 +46,39 @@ Using the password validator changes a user's password in other ways. If you manipulate user passwords through means other than the high-level APIs listed above, you'll need to manually check passwords. + + +Changing the error message +========================== + +To change the error or help message shown to the user, you can set it +in the ``OPTIONS`` dictionary like so: + +.. code-block:: python + + AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'pwned_passwords_django.validators.PwnedPasswordsValidator', + 'OPTIONS': { + 'error_message': 'That password was pwned', + 'help_message': 'Your password can\'t be a commonly used password.', + } + }, + ] + +The amount of times the password has appeared in a breach can also be included +in the error message, including a plural form: + +.. code-block:: python + + AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'pwned_passwords_django.validators.PwnedPasswordsValidator', + 'OPTIONS': { + 'error_message': ( + 'Pwned %(amount)d time', + 'Pwned %(amount)d times', + ) + } + }, + ] diff --git a/pwned_passwords_django/tests/test_validators.py b/pwned_passwords_django/tests/test_validators.py index 25dabe4..2c6da96 100644 --- a/pwned_passwords_django/tests/test_validators.py +++ b/pwned_passwords_django/tests/test_validators.py @@ -2,6 +2,7 @@ from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError +from django.test import override_settings from .. import api from ..validators import PwnedPasswordsValidator @@ -29,7 +30,7 @@ def test_compromised(self): with mock.patch('requests.get', request_mock): with self.assertRaisesMessage( ValidationError, - PwnedPasswordsValidator.PWNED_MESSAGE + PwnedPasswordsValidator.DEFAULT_PWNED_MESSAGE ): validate_password(self.sample_password) request_mock.assert_called_with( @@ -57,3 +58,52 @@ def test_not_compromised(self): ), timeout=api.REQUEST_TIMEOUT, ) + + @override_settings(AUTH_PASSWORD_VALIDATORS=[{ + 'NAME': 'pwned_passwords_django.validators.PwnedPasswordsValidator', + 'OPTIONS': {'error_message': 'Pwned'} + }]) + def test_message_override(self): + """ + Custom message is shown. + + """ + request_mock = self._get_mock() + with mock.patch('requests.get', request_mock): + with self.assertRaisesMessage( + ValidationError, + 'Pwned' + ): + validate_password(self.sample_password) + + @override_settings(AUTH_PASSWORD_VALIDATORS=[{ + 'NAME': 'pwned_passwords_django.validators.PwnedPasswordsValidator', + 'OPTIONS': { + 'error_message': ( + 'Pwned %(amount)d time', + 'Pwned %(amount)d times', + ), + }, + }]) + def test_message_number(self): + """ + Custom message can show the amount of times pwned. + + """ + request_mock_plural = self._get_mock() + request_mock_singular = self._get_mock( + response_text='{}:1'.format(self.sample_password_suffix) + ) + + with mock.patch('requests.get', request_mock_plural): + with self.assertRaisesMessage( + ValidationError, + 'Pwned 3 times' + ): + validate_password(self.sample_password) + with mock.patch('requests.get', request_mock_singular): + with self.assertRaisesMessage( + ValidationError, + 'Pwned 1 time' + ): + validate_password(self.sample_password) diff --git a/pwned_passwords_django/validators.py b/pwned_passwords_django/validators.py index eacb412..0ef7434 100644 --- a/pwned_passwords_django/validators.py +++ b/pwned_passwords_django/validators.py @@ -1,5 +1,5 @@ from django.core.exceptions import ValidationError -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ungettext from . import api @@ -9,14 +9,34 @@ class PwnedPasswordsValidator(object): Password validator which checks the Pwned Passwords database. """ - HELP_MESSAGE = _("Your password can't be a commonly used password.") - PWNED_MESSAGE = _( + DEFAULT_HELP_MESSAGE = _( + "Your password can't be a commonly used password." + ) + DEFAULT_PWNED_MESSAGE = ( "This password is known to have appeared in a public data breach." ) + def __init__(self, error_message=None, help_message=None): + self.help_message = help_message or self.DEFAULT_HELP_MESSAGE + error_message = error_message or self.DEFAULT_PWNED_MESSAGE + # If there is no plural, use the same message for both forms + if not isinstance(error_message, (list, tuple)): + self.error_message = (error_message, error_message) + else: + self.error_message = error_message + def validate(self, password, user=None): - if api.pwned_password(password): - raise ValidationError(self.PWNED_MESSAGE, code='pwned_password') + amount = api.pwned_password(password) + if amount: + raise ValidationError( + ungettext( + self.error_message[0], + self.error_message[1], + amount, + ), + params={'amount': amount}, + code='pwned_password', + ) def get_help_text(self): - return self.HELP_MESSAGE # pragma: no cover + return self.help_message # pragma: no cover