Skip to content
Open
7 changes: 6 additions & 1 deletion openedx/core/djangoapps/user_api/accounts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,12 @@ def create_retirement_request_and_deactivate_account(user):
user.set_unusable_password()
user.save()

# TODO: Unlink social accounts & change password on each IDA.
# Do not unlink/redact social accounts during the initial retirement request.
# If the user cancels retirement during the 14-day cool-off period, they must
# still be able to authenticate using their existing social account. Redacting
# or deleting the social account at this stage would permanently break that
# login path. Therefore, social account unlinking/redaction should only occur
# when retirement is finalized after the cool-off period has elapsed.
# Remove the activation keys sent by email to the user for account activation.
Registration.objects.filter(user=user).delete()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH
from openedx.core.djangoapps.user_api.models import UserRetirementRequest
from openedx.core.djangoapps.user_api.accounts.utils import create_retirement_request_and_deactivate_account
from openedx.core.djangoapps.user_api.models import RetirementState, UserRetirementRequest
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
from openedx.core.djangoapps.user_authn.views.password_reset import (
PASSWORD_RESET_INITIATED,
Expand Down Expand Up @@ -79,6 +80,14 @@ def setUp(self): # pylint: disable=arguments-differ
self.user_bad_passwd.password = UNUSABLE_PASSWORD_PREFIX
self.user_bad_passwd.save()

# Create PENDING retirement state for tests that need it
RetirementState.objects.create(
state_name='PENDING',
state_execution_order=1,
is_dead_end_state=False,
required=True,
)

def setup_request_session_with_token(self, request):
"""
Internal helper to setup request session and add token in session.
Expand Down Expand Up @@ -554,6 +563,47 @@ def test_password_reset_retired_user_fail(self):
assert resp.status_code == 200
assert not User.objects.get(pk=self.user.pk).is_active

def test_password_reset_retired_user_initiation_fail(self):
"""
Tests that a retired user cannot initiate a password reset.
"""
create_retirement_request_and_deactivate_account(self.user)
self.user.refresh_from_db()
assert not self.user.is_active
assert not self.user.has_usable_password()

reset_request = self.request_factory.post('/password_reset/', {'email': self.user.email})
reset_request.user = AnonymousUser()
response = password_reset(reset_request)

# Always return 200 OK to prevent user enumeration while leaving the password unchanged and unusable.
assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8'))
assert response_data['success'] is True
assert len(mail.outbox) == 0

def test_password_reset_retired_user_complete_fail(self):
"""
Tests that a retired user cannot complete password reset even with a submitted form.
"""
create_retirement_request_and_deactivate_account(self.user)
self.user.refresh_from_db()
assert not self.user.is_active
old_password_hash = self.user.password

request_params = {'new_password1': 'new_password1', 'new_password2': 'new_password1'}
confirm_request = self.request_factory.post(self.password_reset_confirm_url, data=request_params)
self.setup_request_session_with_token(confirm_request)
confirm_request.user = self.user

response = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token)

# Always return 200 OK to prevent user enumeration while leaving the password unchanged and unusable.
assert response.status_code == 200
self.user.refresh_from_db()
assert not self.user.has_usable_password()
assert self.user.password == old_password_hash

def test_password_reset_normalize_password(self):
# pylint: disable=anomalous-unicode-escape-in-string
"""
Expand Down
Loading