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:
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user