forms: specialize form for password reset by username (#52013)
This commit is contained in:
parent
5baca2ba03
commit
13cd493740
|
@ -133,8 +133,8 @@ default_settings = dict(
|
|||
A2_PROFILE_DISPLAY_EMPTY_FIELDS=Setting(default=False, definition='Include empty fields in profile view'),
|
||||
A2_HOMEPAGE_URL=Setting(default=None, definition='IdP has no homepage, redirect to this one.'),
|
||||
A2_USER_CAN_RESET_PASSWORD=Setting(default=None, definition='Allow online reset of passwords'),
|
||||
A2_RESET_PASSWORD_ID_LABEL=Setting(
|
||||
default=None, definition='Alternate ID label for the password reset form'
|
||||
A2_USER_CAN_RESET_PASSWORD_BY_USERNAME=Setting(
|
||||
default=False, definition='Allow password reset request by username'
|
||||
),
|
||||
A2_EMAIL_IS_UNIQUE=Setting(default=False, definition='Email of users must be unique'),
|
||||
A2_REGISTRATION_EMAIL_IS_UNIQUE=Setting(
|
||||
|
|
|
@ -20,13 +20,12 @@ from collections import OrderedDict
|
|||
from django import forms
|
||||
from django.contrib.auth import forms as auth_forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.forms import Form
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .. import app_settings, hooks, models, utils
|
||||
from .. import app_settings, hooks, models, utils, validators
|
||||
from ..backends import get_user_queryset
|
||||
from .fields import CheckPasswordField, NewPasswordField, PasswordField
|
||||
from .fields import CheckPasswordField, NewPasswordField, PasswordField, ValidatedEmailField
|
||||
from .utils import NextUrlFormMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -35,16 +34,43 @@ logger = logging.getLogger(__name__)
|
|||
class PasswordResetForm(forms.Form):
|
||||
next_url = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
email = forms.CharField(label=_("Email"), max_length=254)
|
||||
email = ValidatedEmailField(label=_("Email"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if app_settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME:
|
||||
del self.fields['email']
|
||||
self.fields['email_or_username'] = forms.CharField(label=_('Email or username'), max_length=254)
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
if email:
|
||||
self.users = get_user_queryset().filter(email__iexact=email)
|
||||
return email
|
||||
|
||||
def clean_email_or_username(self):
|
||||
email_or_username = self.cleaned_data.get('email_or_username')
|
||||
if email_or_username:
|
||||
self.users = get_user_queryset().filter(
|
||||
models.Q(username__iexact=email_or_username) | models.Q(email__iexact=email_or_username)
|
||||
)
|
||||
try:
|
||||
validators.email_validator(email_or_username)
|
||||
except ValidationError:
|
||||
pass
|
||||
else:
|
||||
self.cleaned_data['email'] = email_or_username
|
||||
return email_or_username
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Generates a one-use only link for resetting password and sends to the
|
||||
user.
|
||||
"""
|
||||
email = self.cleaned_data["email"].strip()
|
||||
users = get_user_queryset().filter(Q(email__iexact=email) | Q(username__iexact=email))
|
||||
active_users = users.filter(is_active=True)
|
||||
email = self.cleaned_data.get('email')
|
||||
email_or_username = self.cleaned_data.get('email_or_username')
|
||||
|
||||
active_users = self.users.filter(is_active=True)
|
||||
for user in active_users:
|
||||
# we don't set the password to a random string, as some users should not have
|
||||
# a password
|
||||
|
@ -52,14 +78,14 @@ class PasswordResetForm(forms.Form):
|
|||
utils.send_password_reset_mail(
|
||||
user, set_random_password=set_random_password, next_url=self.cleaned_data.get('next_url')
|
||||
)
|
||||
for user in users.filter(is_active=False):
|
||||
for user in self.users.filter(is_active=False):
|
||||
logger.info('password reset failed for user "%r": account is disabled', user)
|
||||
utils.send_templated_mail(user, ['authentic2/password_reset_refused'])
|
||||
if not users.exists():
|
||||
if not self.users.exists() and email:
|
||||
logger.info(u'password reset request for "%s", no user found', email)
|
||||
ctx = {'registration_url': utils.make_url('registration_register', absolute=True)}
|
||||
utils.send_templated_mail(email, ['authentic2/password_reset_no_account'], context=ctx)
|
||||
hooks.call_hooks('event', name='password-reset', email=email, users=active_users)
|
||||
hooks.call_hooks('event', name='password-reset', email=email or email_or_username, users=active_users)
|
||||
|
||||
|
||||
class PasswordResetMixin(Form):
|
||||
|
|
|
@ -653,13 +653,6 @@ class PasswordResetView(FormView):
|
|||
'registration/password_reset_form.html',
|
||||
]
|
||||
|
||||
def get_form(self, **kwargs):
|
||||
"""Return an instance of the form to be used in this view."""
|
||||
form = super(PasswordResetView, self).get_form(**kwargs)
|
||||
if app_settings.A2_RESET_PASSWORD_ID_LABEL:
|
||||
form.fields['email'].label = app_settings.A2_RESET_PASSWORD_ID_LABEL
|
||||
return form
|
||||
|
||||
def get_form_kwargs(self, **kwargs):
|
||||
kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs)
|
||||
initial = kwargs.setdefault('initial', {})
|
||||
|
@ -674,7 +667,7 @@ class PasswordResetView(FormView):
|
|||
return ctx
|
||||
|
||||
def form_valid(self, form):
|
||||
email = form.cleaned_data['email']
|
||||
email = form.cleaned_data.get('email') or form.cleaned_data.get('email_or_username')
|
||||
|
||||
# if an email has already been sent, warn once before allowing resend
|
||||
token = models.Token.objects.filter(
|
||||
|
|
|
@ -42,7 +42,7 @@ def test_send_password_reset_email(app, simple_user, mailoutbox):
|
|||
utils.assert_event('user.password.reset', user=simple_user, session=app.session)
|
||||
|
||||
|
||||
def test_view_with_email(app, simple_user, mailoutbox, settings):
|
||||
def test_reset_by_email(app, simple_user, mailoutbox, settings):
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('email', simple_user.email)
|
||||
|
@ -69,19 +69,20 @@ def test_view_with_email(app, simple_user, mailoutbox, settings):
|
|||
app.get(url, status=404)
|
||||
|
||||
|
||||
def test_view_with_username(app, simple_user, mailoutbox, settings):
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('email', simple_user.username)
|
||||
assert len(mailoutbox) == 0
|
||||
settings.DEFAULT_FROM_EMAIL = 'show only addr <noreply@example.net>'
|
||||
resp = resp.form.submit()
|
||||
utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
|
||||
assert resp['Location'].endswith('/instructions/')
|
||||
resp = resp.follow()
|
||||
assert '"noreply@example.net"' in resp.text
|
||||
assert 'show only addr' not in resp.text
|
||||
def test_can_reset_by_username(app, db, simple_user, settings, mailoutbox):
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
assert 'email_or_username' not in resp.form.fields
|
||||
settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME = True
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
assert 'email_or_username' in resp.form.fields
|
||||
|
||||
resp.form.set('email_or_username', simple_user.username)
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'an email has been sent to %s' % simple_user.username in resp
|
||||
assert len(mailoutbox) == 1
|
||||
assert mailoutbox[0].to == [simple_user.email]
|
||||
|
||||
url = utils.get_link_from_mail(mailoutbox[0])
|
||||
relative_url = url.split('testserver')[1]
|
||||
resp = app.get(relative_url, status=200)
|
||||
|
@ -91,20 +92,44 @@ def test_view_with_username(app, simple_user, mailoutbox, settings):
|
|||
# verify user is logged
|
||||
assert str(app.session['_auth_user_id']) == str(simple_user.pk)
|
||||
|
||||
with override_settings(A2_USER_CAN_RESET_PASSWORD=False):
|
||||
url = reverse('password_reset')
|
||||
app.get(url, status=404)
|
||||
|
||||
def test_can_reset_by_username_with_email(app, db, simple_user, settings, mailoutbox):
|
||||
settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME = True
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
resp.form.set('email_or_username', simple_user.email)
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'an email has been sent to %s' % simple_user.username in resp
|
||||
assert len(mailoutbox) == 1
|
||||
|
||||
|
||||
def test_user_filter(app, simple_user, mailoutbox, settings):
|
||||
settings.A2_USER_FILTER = {'username': 'xxx'} # will not match simple_user
|
||||
def test_reset_by_email_no_account(app, db, mailoutbox):
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
resp.form.set('email', 'john.doe@example.com')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('email', simple_user.email)
|
||||
assert 'an email has been sent to john.doe@example.com' in resp
|
||||
assert len(mailoutbox) == 1
|
||||
assert 'no account was found' in mailoutbox[0].body
|
||||
|
||||
|
||||
def test_can_reset_by_username_no_account(app, db, settings, mailoutbox):
|
||||
settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME = True
|
||||
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
resp.form.set('email_or_username', 'john.doe')
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'an email has been sent to john.doe' in resp
|
||||
assert len(mailoutbox) == 0
|
||||
resp = resp.form.submit()
|
||||
assert 'no account was found associated with this address' in mailoutbox[0].body
|
||||
|
||||
|
||||
def test_can_reset_by_username_no_account_email(app, db, settings, mailoutbox):
|
||||
settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME = True
|
||||
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
resp.form.set('email_or_username', 'john.doe@example.com')
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'an email has been sent to john.doe' in resp
|
||||
assert len(mailoutbox) == 1
|
||||
|
||||
|
||||
def test_user_exclude(app, simple_user, mailoutbox, settings):
|
||||
|
@ -150,3 +175,10 @@ def test_send_password_reset_email_disabled_account(app, simple_user, mailoutbox
|
|||
mail = mailoutbox[0]
|
||||
assert mail.subject == 'Your account on testserver is disabled'
|
||||
assert 'your account has been disabled on this server' in mail.body
|
||||
|
||||
|
||||
def test_email_validation(app, db):
|
||||
resp = app.get('/accounts/password/reset/')
|
||||
resp.form.set('email', 'coin@')
|
||||
resp = resp.form.submit()
|
||||
assert 'Enter a valid email address.' in resp
|
||||
|
|
Loading…
Reference in New Issue