From c7c3ef694c7d2c5b24369d8cb2de35766cdfe3fd Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 20 Jul 2017 01:49:07 -0400 Subject: [PATCH] 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 --- cms/envs/common.py | 4 +++ .../student_account/test/test_views.py | 25 +++++++++++++++++++ lms/djangoapps/student_account/views.py | 25 +++++++++++++++++-- lms/envs/common.py | 4 +++ .../registration/password_reset_email.html | 6 +++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 32310f9c24..f6221f1590 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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, diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 37722ff1fc..f5f9a20e1a 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -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 diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 2358cf8edc..7d4b803011 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -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)) diff --git a/lms/envs/common.py b/lms/envs/common.py index 42898fae2c..b62ed7413a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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 diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index c41419d4a5..611863b5b1 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -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 %}