Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow overriding of messages #5

Merged
merged 3 commits into from May 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/validator.rst
Expand Up @@ -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',
)
}
},
]
31 changes: 26 additions & 5 deletions src/pwned_passwords_django/validators.py
@@ -1,5 +1,6 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext

from . import api

Expand All @@ -9,14 +10,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)
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
52 changes: 51 additions & 1 deletion tests/test_validators.py
@@ -1,6 +1,7 @@
import mock
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.test import override_settings

from pwned_passwords_django import api
from pwned_passwords_django.validators import PwnedPasswordsValidator
Expand Down Expand Up @@ -28,7 +29,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(
Expand Down Expand Up @@ -58,3 +59,52 @@ def test_not_compromised(self):
headers=self.user_agent,
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)