Add optional password reset failure emails

This change allows Open edX operators to enable password reset
failure email notifications. This is mainly useful for
explicitly informing users that there's no account associated
with their email, so that users can be sure that there wasn't
a problem with the email system.

OSPR-1832
This commit is contained in:
Brandon DeRosier
2017-07-20 01:49:07 -04:00
parent 8844f9662b
commit c7c3ef694c
5 changed files with 62 additions and 2 deletions

View File

@@ -276,6 +276,10 @@ FEATURES = {
# Whether or not the dynamic EnrollmentTrackUserPartition should be registered.
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION': True,
# Whether to send an email for failed password reset attempts or not. This is mainly useful for notifying users
# that they don't have an account associated with email addresses they believe they've registered with.
'ENABLE_PASSWORD_RESET_FAILURE_EMAIL': False,
# Whether archived courses (courses with end dates in the past) should be
# shown in Studio in a separate list.
'ENABLE_SEPARATE_ARCHIVED_COURSES': True,

View File

@@ -51,6 +51,9 @@ from openedx.core.djangoapps.user_api.errors import UserAPIInternalError
LOGGER_NAME = 'audit'
User = get_user_model() # pylint:disable=invalid-name
FEATURES_WITH_FAILED_PASSWORD_RESET_EMAIL = settings.FEATURES.copy()
FEATURES_WITH_FAILED_PASSWORD_RESET_EMAIL['ENABLE_PASSWORD_RESET_FAILURE_EMAIL'] = True
@ddt.ddt
class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
@@ -140,6 +143,28 @@ class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
self._change_password()
self.assertRaises(UserAPIInternalError)
@override_settings(FEATURES=FEATURES_WITH_FAILED_PASSWORD_RESET_EMAIL)
def test_password_reset_failure_email(self):
"""Test that a password reset failure email notification is sent, when enabled."""
# Log the user out
self.client.logout()
bad_email = 'doesnotexist@example.com'
response = self._change_password(email=bad_email)
self.assertEqual(response.status_code, 200)
# Check that an email was sent
self.assertEqual(len(mail.outbox), 1)
# Verify that the body contains the failed password reset message
email_body = mail.outbox[0].body
self.assertIn(
'However, there is currently no user account associated with your email address: {email}'.format(
email=bad_email
),
email_body,
)
@ddt.data(True, False)
def test_password_change_logged_out(self, send_email):
# Log the user out

View File

@@ -9,9 +9,11 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.core.mail import send_mail
from django.core.urlresolvers import resolve, reverse
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import redirect
from django.template import loader
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
@@ -216,6 +218,25 @@ def password_change_request_handler(request):
AUDIT_LOG.info("Invalid password reset attempt")
# Increment the rate limit counter
limiter.tick_bad_request_counter(request)
# If enabled, send an email saying that a password reset was attempted, but that there is
# no user associated with the email
if configuration_helpers.get_value('ENABLE_PASSWORD_RESET_FAILURE_EMAIL',
settings.FEATURES['ENABLE_PASSWORD_RESET_FAILURE_EMAIL']):
context = {
'failed': True,
'email_address': email,
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
}
subject = loader.render_to_string('emails/password_reset_subject.txt', context)
subject = ''.join(subject.splitlines())
message = loader.render_to_string('registration/password_reset_email.html', context)
from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
try:
send_mail(subject, message, from_email, [email])
except Exception: # pylint: disable=broad-except
log.exception(u'Unable to send password reset failure email notification from "%s"', from_email)
except UserAPIInternalError as err:
log.exception('Error occured during password change for user {email}: {error}'
.format(email=email, error=err))

View File

@@ -418,6 +418,10 @@ FEATURES = {
# Whether HTML XBlocks/XModules return HTML content with the Course Blocks API student_view_data
'ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA': False,
# Whether to send an email for failed password reset attempts or not. This is mainly useful for notifying users
# that they don't have an account associated with email addresses they believe they've registered with.
'ENABLE_PASSWORD_RESET_FAILURE_EMAIL': False,
}
# Settings for the course reviews tool template and identification key, set either to None to disable course reviews

View File

@@ -1,6 +1,11 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ platform_name }}.{% endblocktrans %}
{% if failed %}
{% blocktrans %}However, there is currently no user account associated with your email address: {{ email_address }}.{% endblocktrans %}
{% trans "If you did not request this change, you can ignore this email." %}
{% else %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ site_name }}{% url 'password_reset_confirm' uidb36=uid token=token %}
@@ -9,6 +14,7 @@
{% trans "If you didn't request this change, you can disregard this email - we have not yet reset your password." %}
{% trans "Thanks for using our site!" %}
{% endif %}
{% blocktrans %}The {{ platform_name }} Team{% endblocktrans %}