Merge pull request #19050 from edx/diana/password-reset-email

Send password reset email after password compliance failure.
This commit is contained in:
Diana Huang
2018-10-05 09:37:07 -04:00
committed by GitHub
6 changed files with 49 additions and 26 deletions

View File

@@ -30,6 +30,34 @@ from student.models import CourseEnrollmentAllowed, email_exists_or_retired
from util.password_policy_validators import password_max_length, password_min_length, validate_password
def send_password_reset_email_for_user(user, request):
"""
Send out a password reset email for the given user.
"""
site = get_current_site()
message_context = get_base_template_context(site)
message_context.update({
'request': request, # Used by google_analytics_tracking_pixel
# TODO: This overrides `platform_name` from `get_base_template_context` to make the tests passes
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'reset_link': '{protocol}://{site}{link}'.format(
protocol='https' if request.is_secure() else 'http',
site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
link=reverse('password_reset_confirm', kwargs={
'uidb36': int_to_base36(user.id),
'token': default_token_generator.make_token(user),
}),
)
})
msg = PasswordReset().personalize(
recipient=Recipient(user.username, user.email),
language=get_user_preference(user, LANGUAGE_KEY),
user_context=message_context,
)
ace.send(msg)
class PasswordResetFormNoActive(PasswordResetForm):
error_messages = {
'unknown': _("That e-mail address doesn't have an associated "
@@ -64,29 +92,7 @@ class PasswordResetFormNoActive(PasswordResetForm):
user.
"""
for user in self.users_cache:
site = get_current_site()
message_context = get_base_template_context(site)
message_context.update({
'request': request, # Used by google_analytics_tracking_pixel
# TODO: This overrides `platform_name` from `get_base_template_context` to make the tests passes
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'reset_link': '{protocol}://{site}{link}'.format(
protocol='https' if use_https else 'http',
site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
link=reverse('password_reset_confirm', kwargs={
'uidb36': int_to_base36(user.id),
'token': token_generator.make_token(user),
}),
)
})
msg = PasswordReset().personalize(
recipient=Recipient(user.username, user.email),
language=get_user_preference(user, LANGUAGE_KEY),
user_context=message_context,
)
ace.send(msg)
send_password_reset_email_for_user(user, request)
class TrueCheckbox(widgets.CheckboxInput):

View File

@@ -20,6 +20,11 @@ class AceCommonConfig(AppConfig):
SettingsType.AWS: {PluginSettings.RELATIVE_PATH: u'settings.aws'},
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: u'settings.common'},
SettingsType.DEVSTACK: {PluginSettings.RELATIVE_PATH: u'settings.devstack'},
},
ProjectType.CMS: {
SettingsType.AWS: {PluginSettings.RELATIVE_PATH: u'settings.aws'},
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: u'settings.common'},
SettingsType.DEVSTACK: {PluginSettings.RELATIVE_PATH: u'settings.devstack'},
}
}
}

View File

@@ -2,6 +2,7 @@
Context dictionary for templates that use the ace_common base template.
"""
from django.conf import settings
from django.core.urlresolvers import NoReverseMatch
from django.urls import reverse
from edxmako.shortcuts import marketing_link
@@ -12,10 +13,17 @@ def get_base_template_context(site):
"""
Dict with entries needed for all templates that use the base template.
"""
# When on LMS and a dashboard is available, use that as the dashboard url.
# Otherwise, use the home url instead.
try:
dashboard_url = reverse('dashboard')
except NoReverseMatch:
dashboard_url = reverse('home')
return {
# Platform information
'homepage_url': marketing_link('ROOT'),
'dashboard_url': reverse('dashboard'),
'dashboard_url': dashboard_url,
'template_revision': getattr(settings, 'EDX_PLATFORM_REVISION', None),
'platform_name': get_config_value_from_site_or_settings(
'PLATFORM_NAME',

View File

@@ -83,8 +83,7 @@ def enforce_compliance_on_login(user, password):
_capitalize_first(_(
'{platform_name} now requires more complex passwords. Your current password does not meet the new '
'requirements. You must change your password by {deadline} to be able to continue using the site. '
'To change your password, select the dropdown menu icon next to your username, then select "Account". '
'You can reset your password from this page. Thank you for helping us keep your data safe.'
'A password reset e-mail has been sent to the address associated with this account.'
).format(
platform_name=settings.PLATFORM_NAME,
deadline=strftime_localized(deadline, DEFAULT_SHORT_DATE_FORMAT)

View File

@@ -32,6 +32,7 @@ from student.models import (
PasswordHistory,
)
from student.views import send_reactivation_email_for_user
from student.forms import send_password_reset_email_for_user
import third_party_auth
from third_party_auth import pipeline, provider
from util.json_request import JsonResponse
@@ -146,6 +147,7 @@ def _enforce_password_policy_compliance(request, user):
# Allow login, but warn the user that they will be required to reset their password soon.
PageLevelMessages.register_warning_message(request, e.message)
except password_policy_compliance.NonCompliantPasswordException as e:
send_password_reset_email_for_user(user, request)
# Prevent the login attempt.
raise AuthFailedError(e.message)

View File

@@ -7,6 +7,7 @@ import unittest
from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core import mail
from django.urls import NoReverseMatch, reverse
from django.http import HttpResponse, HttpResponseBadRequest
from django.test.client import Client
@@ -462,6 +463,8 @@ class LoginTest(CacheIsolationTestCase):
)
response_content = json.loads(response.content)
self.assertFalse(response_content.get('success'))
self.assertEqual(len(mail.outbox), 1)
self.assertIn('Password reset', mail.outbox[0].subject)
@override_settings(PASSWORD_POLICY_COMPLIANCE_ROLLOUT_CONFIG={'ENFORCE_COMPLIANCE_ON_LOGIN': True})
def test_check_password_policy_compliance_warning(self):