Unverified Commit 3d087d5c by Brian Beggs Committed by GitHub

Merge pull request #15511 from proversity-org/proversity/add-recover-password-endpoint

Add recover password endpoint
parents 622ee7a7 9cf0b335
......@@ -138,6 +138,18 @@ class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
self._change_password()
self.assertRaises(UserAPIInternalError)
@ddt.data(
({'email': 'walter@graymattertech.com'}, 200),
({'email': ''}, 400),
({}, 400),
)
@ddt.unpack
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
def test_password_recover_api(self, password_recover_info, expected_status):
self.client.logout()
response = self._recover_password_through_api(**password_recover_info)
self.assertEqual(response.status_code, expected_status)
@ddt.data(True, False)
def test_password_change_logged_out(self, send_email):
# Log the user out
......@@ -231,6 +243,15 @@ class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
return self.client.post(path=reverse('password_change_request'), data=data)
def _recover_password_through_api(self, email=None):
"""Request to change the user's password. """
data = {}
if email:
data['email'] = email
return self.client.post(path=reverse('recover_password_api'), data=data)
def _create_dop_tokens(self, user=None):
"""Create dop access token for given user if user provided else for default user."""
if not user:
......
......@@ -10,5 +10,14 @@ urlpatterns = [
if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'):
urlpatterns += [
url(r'^password$', views.password_change_request_handler, name='password_change_request'),
url(
r'^password$',
views.password_change_request_handler,
name='password_change_request'
),
url(
r'^recover-password$',
views.RecoverPasswordView.as_view(),
name="recover_password_api"
),
]
......@@ -8,6 +8,7 @@ from datetime import datetime
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
......@@ -34,10 +35,12 @@ from openedx.core.djangoapps.user_api.api import (
get_login_session_form,
get_password_reset_form
)
from openedx.core.djangoapps.user_api.errors import (
UserNotFound,
UserAPIInternalError
)
from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES
from openedx.features.enterprise_support.api import enterprise_customer_for_request
......@@ -50,6 +53,11 @@ from third_party_auth.decorators import xframe_allow_whitelisted
from util.bad_request_rate_limiter import BadRequestRateLimiter
from util.date_utils import strftime_localized
from rest_framework.response import Response
from rest_framework import status
from rest_framework import views
from rest_framework.decorators import api_view
AUDIT_LOG = logging.getLogger("audit")
log = logging.getLogger(__name__)
User = get_user_model() # pylint:disable=invalid-name
......@@ -173,6 +181,7 @@ def login_and_registration_form(request, initial_mode="login"):
@require_http_methods(['POST'])
@api_view(['POST'])
def password_change_request_handler(request):
"""Handle password change requests originating from the account page.
......@@ -198,32 +207,24 @@ def password_change_request_handler(request):
"""
limiter = BadRequestRateLimiter()
if limiter.is_rate_limit_exceeded(request):
AUDIT_LOG.warning("Password reset rate limit exceeded")
return HttpResponseForbidden()
user = request.user
# Prefer logged-in user's email
email = user.email if user.is_authenticated() else request.POST.get('email')
email =\
user.email if user.is_authenticated() else request.POST.get('email')
error_message =\
_("Some error occured during password change. Please try again")
if email:
try:
request_password_change(email, request.is_secure())
user = user if user.is_authenticated() else User.objects.get(email=email)
destroy_oauth_tokens(user)
except UserNotFound:
AUDIT_LOG.info("Invalid password reset attempt")
# Increment the rate limit counter
limiter.tick_bad_request_counter(request)
except UserAPIInternalError as err:
log.exception('Error occured during password change for user {email}: {error}'
.format(email=email, error=err))
return HttpResponse(_("Some error occured during password change. Please try again"), status=500)
return HttpResponse(status=200)
else:
return HttpResponseBadRequest(_("No email address provided."))
status_response = change_password(request, email)
if status_response == 500:
return HttpResponse(error_message, status=status_response)
elif status_response == 403:
return HttpResponseForbidden()
return HttpResponse(status=status_response)
return HttpResponseBadRequest(_("No email address provided."))
def update_context_for_enterprise(request, context):
......@@ -595,3 +596,66 @@ def account_settings_context(request):
} for state in auth_states if state.provider.display_for_login or state.has_account]
return context
class RecoverPasswordView(views.APIView):
"""
Resets a password of a user by proving the email address
Example Requests:
POST /account/recover-password
{"email": "staff@example.com"}
Response Values:
HttpResponse: 200 if the password was reset correctly
HttpResponse: 400 if email wasn't provided
HttpResponse: 403 if the client has been rate limited
HttpResponse: 405 if using an unsupported HTTP method
"""
def post(self, request, *args, **kwargs):
"""
Makes the request to change the password
"""
email = request.data.get('email')
if email:
return Response(status=change_password(request, email))
return Response(status=status.HTTP_400_BAD_REQUEST)
def change_password(request, email):
"""
Changes user's password by providing email.
Args:
request (HTTPRequest): The request to change user's email
email (String): The user email
Returns:
status code of Response
Options:
- 200
- 403
- 500
"""
limiter = BadRequestRateLimiter()
if limiter.is_rate_limit_exceeded(request):
AUDIT_LOG.warning("Password reset rate limit exceeded")
return status.HTTP_403_FORBIDDEN
try:
user = User.objects.get(email=email)
request_password_change(email, request.is_secure())
destroy_oauth_tokens(user)
except User.DoesNotExist:
AUDIT_LOG.info("Invalid password reset attempt")
# Increment the rate limit counter
limiter.tick_bad_request_counter(request)
except UserAPIInternalError as err:
log.exception(
'Error occured during password change for user {email}: {error}'
.format(email=email, error=err)
)
return status.HTTP_500_INTERNAL_SERVER_ERROR
return status.HTTP_200_OK
......@@ -375,7 +375,6 @@ def request_password_change(email, is_secure):
Args:
email (str): An email address
orig_host (str): An originating host, extracted from a request with get_host
is_secure (bool): Whether the request was made with HTTPS
Returns:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment