Merge pull request #25262 from edx/adeel/van_88_adding_password_reset_endpoint

Add new password reset endpoint for logistration MFE.
This commit is contained in:
adeel khan
2020-10-16 11:17:50 +05:00
committed by GitHub
3 changed files with 142 additions and 2 deletions

View File

@@ -63,9 +63,17 @@ urlpatterns = [
name='password_reset_confirm',
),
url(r'^account/password$', password_reset.password_change_request_handler, name='password_change_request'),
# logistration MFE flow
url(r'^user_api/v1/account/password_reset/token/validate/$', password_reset.password_reset_token_validate,
name="user_api_password_reset_token_validate"),
# logistration MFE reset flow
url(
r'^password/reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
password_reset.password_reset_logistration,
name='logistration_password_reset',
),
]
# password reset django views (see above for password reset views)

View File

@@ -12,7 +12,7 @@ from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import INTERNAL_RESET_SESSION_TOKEN, PasswordResetConfirmView
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import ValidationError
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -651,6 +651,9 @@ def password_reset_token_validate(request):
return JsonResponse({'is_valid': is_valid})
user = User.objects.get(id=uid_int)
if UserRetirementRequest.has_user_requested_retirement(user):
return JsonResponse({'is_valid': is_valid})
is_valid = default_token_generator.check_token(user, token[1])
if is_valid and not user.is_active:
user.is_active = True
@@ -659,3 +662,63 @@ def password_reset_token_validate(request):
AUDIT_LOG.exception("Invalid password reset confirm token")
return JsonResponse({'is_valid': is_valid})
def _check_token_has_required_values(uidb36, token):
"""
Helper function to test that token
string passed has the required kwargs needed
to process token validation.
"""
if not uidb36 or not token:
return False, None
try:
uid_int = base36_to_int(uidb36)
except ValueError:
return False, None
return True, uid_int
@require_POST
@ensure_csrf_cookie
def password_reset_logistration(request, **kwargs):
"""Reset learner password using passed token and new credentials"""
reset_status = False
uidb36 = kwargs.get('uidb36')
token = kwargs.get('token')
has_required_values, uid_int = _check_token_has_required_values(uidb36, token)
if not has_required_values:
AUDIT_LOG.exception("Invalid password reset confirm token")
return JsonResponse({'reset_status': reset_status})
request.POST = request.POST.copy()
request.POST['new_password1'] = normalize_password(request.POST['new_password1'])
request.POST['new_password2'] = normalize_password(request.POST['new_password2'])
password = request.POST['new_password1']
try:
user = User.objects.get(id=uid_int)
if not default_token_generator.check_token(user, token):
AUDIT_LOG.exception("Token validation failed")
return JsonResponse({'reset_status': reset_status})
validate_password(password, user=user)
form = SetPasswordForm(user, request.POST)
if form.is_valid():
form.save()
reset_status = True
except ValidationError as err:
AUDIT_LOG.exception("Password validation failed")
error_status = {
'reset_status': reset_status,
'err_msg': ' '.join(err.messages)
}
return JsonResponse(error_status)
except Exception: # pylint: disable=broad-except
AUDIT_LOG.exception("Setting new password failed")
return JsonResponse({'reset_status': reset_status})

View File

@@ -35,7 +35,7 @@ from openedx.core.djangoapps.user_api.models import UserRetirementRequest
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH
from openedx.core.djangoapps.user_authn.views.password_reset import (
SETTING_CHANGE_INITIATED, password_reset,
SETTING_CHANGE_INITIATED, password_reset, password_reset_logistration,
PasswordResetConfirmWrapper)
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import TEST_PASSWORD, UserFactory
@@ -713,3 +713,72 @@ class PasswordResetTokenValidateViewTest(UserAPITestCase):
self.user = User.objects.get(pk=self.user.pk)
self.assertFalse(self.user.is_active)
@ddt.ddt
@unittest.skipUnless(
settings.ROOT_URLCONF == "lms.urls",
"reset password tests should only run in LMS"
)
class ResetPasswordAPITests(CacheIsolationTestCase):
"""Tests of the logistration API's password reset endpoint. """
request_factory = RequestFactory()
ENABLED_CACHES = ['default']
def setUp(self):
super(ResetPasswordAPITests, self).setUp()
self.user = UserFactory.create()
self.user.save()
self.token = default_token_generator.make_token(self.user)
self.uidb36 = int_to_base36(self.user.id)
def create_reset_request(self, uidb36, token, new_password2='new_password1'):
"""Helper to create reset password post request"""
request_param = {'new_password1': 'new_password1', 'new_password2': new_password2}
post_request = self.request_factory.post(
reverse(
"logistration_password_reset",
kwargs={"uidb36": uidb36, "token": token}
),
request_param
)
return post_request
@ddt.data(
(None, None, True),
(None, 'invalid_token', False),
)
@ddt.unpack
def test_password_reset_request(self, uidb36, token, status):
"""Tests password reset request with valid/invalid token"""
uidb36 = uidb36 or self.uidb36
token = token or self.token
post_request = self.create_reset_request(uidb36, token)
post_request.user = AnonymousUser()
json_response = password_reset_logistration(post_request, uidb36=uidb36, token=token)
json_response = json.loads(json_response.content.decode('utf-8'))
self.assertEqual(json_response.get('reset_status'), status)
def test_none_token_in_password_reset_request(self):
"""
Test that user should not be able to reset password through no token/uidb36
"""
uidb36 = None
token = None
post_request = self.create_reset_request(self.uidb36, self.token)
post_request.user = AnonymousUser()
self.assertRaises(Exception, password_reset_logistration(post_request, uidb36=uidb36, token=token))
def test_password_mismatch_in_reset_request(self):
"""
Test that user should not be able to reset password with password mismatch
"""
post_request = self.create_reset_request(self.uidb36, self.token, 'new_password2')
post_request.user = AnonymousUser()
json_response = password_reset_logistration(post_request, uidb36=self.uidb36, token=self.token)
json_response = json.loads(json_response.content.decode('utf-8'))
self.assertFalse(json_response.get('reset_status'))