Merge pull request #19794 from edx/bessiesteinberg/ent-1512
ENT-1512: Make Recovery Email Retireable
This commit is contained in:
@@ -2735,6 +2735,10 @@ class AccountRecoveryManager(models.Manager):
|
||||
class AccountRecovery(models.Model):
|
||||
"""
|
||||
Model for storing information for user's account recovery in case of access loss.
|
||||
|
||||
.. pii: the field named secondary_email contains pii, retired in the `DeactivateLogoutView`
|
||||
.. pii_types: email_address
|
||||
.. pii_retirement: local_api
|
||||
"""
|
||||
user = models.OneToOneField(User, related_name='account_recovery', on_delete=models.CASCADE)
|
||||
secondary_email = models.EmailField(
|
||||
@@ -2761,3 +2765,22 @@ class AccountRecovery(models.Model):
|
||||
self.secondary_email = email
|
||||
self.is_active = False
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def retire_recovery_email(cls, user_id):
|
||||
"""
|
||||
Retire user's recovery/secondary email as part of GDPR Phase I.
|
||||
Returns 'True'
|
||||
|
||||
If an AccountRecovery record is found for this user it will be deleted,
|
||||
if it is not found it is assumed this table has no PII for the given user.
|
||||
|
||||
:param user_id: int
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
cls.objects.get(user_id=user_id).delete()
|
||||
except cls.DoesNotExist:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import cache
|
||||
from django.db.models import signals
|
||||
from django.db.models.functions import Lower
|
||||
from django.test import TestCase
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
@@ -26,11 +25,13 @@ from student.models import (
|
||||
PendingEmailChange,
|
||||
ManualEnrollmentAudit,
|
||||
ALLOWEDTOENROLL_TO_ENROLLED,
|
||||
PendingNameChange
|
||||
PendingNameChange,
|
||||
AccountRecovery
|
||||
)
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory, AccountRecoveryFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -383,3 +384,24 @@ class TestManualEnrollmentAudit(SharedModuleStoreTestCase):
|
||||
self.assertFalse(ManualEnrollmentAudit.objects.filter(enrollment=enrollment).exclude(
|
||||
reason=""
|
||||
))
|
||||
|
||||
|
||||
class TestAccountRecovery(TestCase):
|
||||
"""
|
||||
Tests for the AccountRecovery Model
|
||||
"""
|
||||
|
||||
def test_retire_recovery_email(self):
|
||||
"""
|
||||
Assert that Account Record for a given user is deleted when `retire_recovery_email` is called
|
||||
"""
|
||||
# Create user and associated recovery email record
|
||||
user = UserFactory()
|
||||
AccountRecoveryFactory(user=user)
|
||||
assert len(AccountRecovery.objects.filter(user_id=user.id)) == 1
|
||||
|
||||
# Retire recovery email
|
||||
AccountRecovery.retire_recovery_email(user_id=user.id)
|
||||
|
||||
# Assert that there is no longer an AccountRecovery record for this user
|
||||
assert len(AccountRecovery.objects.filter(user_id=user.id)) == 0
|
||||
|
||||
@@ -64,6 +64,7 @@ from student.models import (
|
||||
UserProfile,
|
||||
get_retired_username_by_username,
|
||||
get_retired_email_by_email,
|
||||
AccountRecovery,
|
||||
)
|
||||
from student.tests.factories import (
|
||||
ContentTypeFactory,
|
||||
@@ -71,7 +72,8 @@ from student.tests.factories import (
|
||||
PendingEmailChangeFactory,
|
||||
PermissionFactory,
|
||||
SuperuserFactory,
|
||||
UserFactory
|
||||
UserFactory,
|
||||
AccountRecoveryFactory,
|
||||
)
|
||||
|
||||
from ..views import AccountRetirementView, USER_PROFILE_PII
|
||||
@@ -221,6 +223,24 @@ class TestDeactivateLogout(RetirementTestCase):
|
||||
# ensure that it's been sent to the correct email address
|
||||
self.assertIn(self.test_user.email, mail.outbox[0].to)
|
||||
|
||||
def test_user_can_deactivate_secondary_email(self):
|
||||
"""
|
||||
Verify that if a user has a secondary/recovery email that record will be deleted
|
||||
if the user requests a retirement
|
||||
"""
|
||||
# Create secondary/recovery email for test user
|
||||
AccountRecoveryFactory(user=self.test_user)
|
||||
# Assert that there is an secondary/recovery email for test user
|
||||
self.assertEqual(len(AccountRecovery.objects.filter(user_id=self.test_user.id)), 1)
|
||||
|
||||
self.client.login(username=self.test_user.username, password=self.test_password)
|
||||
headers = build_jwt_headers(self.test_user)
|
||||
response = self.client.post(self.url, self.build_post(self.test_password), **headers)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Assert that there is no longer a secondary/recovery email for test user
|
||||
self.assertEqual(len(AccountRecovery.objects.filter(user_id=self.test_user.id)), 0)
|
||||
|
||||
def test_password_mismatch(self):
|
||||
"""
|
||||
Verify that the user submitting a mismatched password results in
|
||||
|
||||
@@ -47,6 +47,7 @@ from openedx.core.djangolib.oauth2_retirement_utils import retire_dot_oauth2_mod
|
||||
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.parsers import MergePatchParser
|
||||
from student.models import (
|
||||
AccountRecovery,
|
||||
CourseEnrollment,
|
||||
ManualEnrollmentAudit,
|
||||
PendingNameChange,
|
||||
@@ -412,6 +413,7 @@ class DeactivateLogoutView(APIView):
|
||||
# Delete OAuth tokens associated with the user.
|
||||
retire_dop_oauth2_models(request.user)
|
||||
retire_dot_oauth2_models(request.user)
|
||||
AccountRecovery.retire_recovery_email(request.user.id)
|
||||
|
||||
try:
|
||||
# Send notification email to user
|
||||
|
||||
Reference in New Issue
Block a user