From 071e3fbf719f101f2d3fe4e135b454feafea2da2 Mon Sep 17 00:00:00 2001 From: ktyagiapphelix2u Date: Fri, 10 Apr 2026 07:54:23 +0000 Subject: [PATCH 1/4] fix: retirement PII leaks by redacting pending secondary email/name data --- common/djangoapps/student/models/user.py | 37 ++++++++++++++++--- .../djangoapps/student/tests/test_models.py | 24 ++++++++++++ common/djangoapps/student/views/management.py | 7 +++- .../accounts/tests/test_retirement_views.py | 35 ++++++++++++++++++ .../djangoapps/user_api/accounts/utils.py | 8 +++- .../djangoapps/user_api/accounts/views.py | 12 ++++++ .../management/commands/retire_user.py | 8 +++- .../management/tests/test_retire_user.py | 17 +++++++++ 8 files changed, 139 insertions(+), 9 deletions(-) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 8cd1fcef7aa4..5d16632c9084 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -933,14 +933,34 @@ class PendingSecondaryEmailChange(DeletableByUserValue, models.Model): """ This model keeps track of pending requested changes to a user's secondary email address. - .. pii: Contains new_secondary_email, not currently retired + .. pii: Contains new_secondary_email, retired in `DeactivateLogoutView` .. pii_types: email_address - .. pii_retirement: retained + .. pii_retirement: local_api """ user = models.OneToOneField(User, unique=True, db_index=True, on_delete=models.CASCADE) new_secondary_email = models.CharField(blank=True, max_length=255, db_index=True) activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) + @classmethod + def retire_pending_secondary_email(cls, user_id): + """ + Retire a pending secondary email change row for a user. + + Redacts the email before deletion so any downstream soft-delete mirror does + not retain the original secondary email address in the final row image. + """ + try: + pending_secondary_email = cls.objects.get(user_id=user_id) + except cls.DoesNotExist: + return True + + pending_secondary_email.new_secondary_email = get_retired_email_by_email( + pending_secondary_email.new_secondary_email + ) + pending_secondary_email.save(update_fields=['new_secondary_email']) + pending_secondary_email.delete() + return True + class LoginFailures(models.Model): """ @@ -1690,16 +1710,21 @@ 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. + If an AccountRecovery record is found for this user it will be redacted and + 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() + account_recovery = cls.objects.get(user_id=user_id) except cls.DoesNotExist: - pass + return True + + account_recovery.secondary_email = get_retired_email_by_email(account_recovery.secondary_email) + account_recovery.is_active = False + account_recovery.save(update_fields=['secondary_email', 'is_active']) + account_recovery.delete() return True diff --git a/common/djangoapps/student/tests/test_models.py b/common/djangoapps/student/tests/test_models.py index 02df1a6714c6..81b747cdf14d 100644 --- a/common/djangoapps/student/tests/test_models.py +++ b/common/djangoapps/student/tests/test_models.py @@ -28,6 +28,7 @@ ManualEnrollmentAudit, PendingEmailChange, PendingNameChange, + PendingSecondaryEmailChange, UserAttribute, UserCelebration, UserProfile @@ -745,6 +746,29 @@ def test_retire_recovery_email(self): assert len(AccountRecovery.objects.filter(user_id=user.id)) == 0 +class TestPendingSecondaryEmailChange(TestCase): + """Tests for retiring PendingSecondaryEmailChange records.""" + + def test_retire_pending_secondary_email(self): + """Assert that pending secondary email records are deleted for retired users.""" + user = UserFactory() + PendingSecondaryEmailChange.objects.create( + user=user, + new_secondary_email='new-secondary@example.com', + activation_key='a' * 32, + ) + assert len(PendingSecondaryEmailChange.objects.filter(user_id=user.id)) == 1 + + PendingSecondaryEmailChange.retire_pending_secondary_email(user_id=user.id) + + assert len(PendingSecondaryEmailChange.objects.filter(user_id=user.id)) == 0 + + def test_retire_pending_secondary_email_when_no_record(self): + """Assert retirement cleanup returns True when no pending secondary row exists.""" + user = UserFactory() + assert PendingSecondaryEmailChange.retire_pending_secondary_email(user_id=user.id) is True + + @ddt.ddt class TestUserPostSaveCallback(SharedModuleStoreTestCase): """ diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 4cf8fad8aea5..0204cb4a1a6e 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -82,7 +82,8 @@ UserSignupSource, UserStanding, create_comments_service_user, - email_exists_or_retired + email_exists_or_retired, + get_retired_email_by_email, ) from common.djangoapps.student.signals import REFUND_ORDER from common.djangoapps.util.db import outer_atomic @@ -862,6 +863,10 @@ def activate_secondary_email(request, key): 'secondary_email': pending_secondary_email_change.new_secondary_email }) + pending_secondary_email_change.new_secondary_email = get_retired_email_by_email( + pending_secondary_email_change.new_secondary_email + ) + pending_secondary_email_change.save(update_fields=['new_secondary_email']) pending_secondary_email_change.delete() return render_to_response("secondary_email_change_successful.html") diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index 5dc1f170c7f6..38aa857a8850 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -31,6 +31,7 @@ ManualEnrollmentAudit, PendingEmailChange, PendingNameChange, + PendingSecondaryEmailChange, Registration, SocialLink, UserProfile, @@ -235,6 +236,24 @@ def test_user_can_deactivate_secondary_email(self): # Assert that there is no longer a secondary/recovery email for test user assert len(AccountRecovery.objects.filter(user_id=self.test_user.id)) == 0 + def test_user_can_deactivate_pending_secondary_email_change(self): + """ + Verify that pending secondary email change records are removed when a user retires. + """ + PendingSecondaryEmailChange.objects.create( + user=self.test_user, + new_secondary_email='pending-secondary@example.com', + activation_key='b' * 32, + ) + assert len(PendingSecondaryEmailChange.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) + assert response.status_code == status.HTTP_204_NO_CONTENT + + assert len(PendingSecondaryEmailChange.objects.filter(user_id=self.test_user.id)) == 0 + def test_password_mismatch(self): """ Verify that the user submitting a mismatched password results in @@ -1393,6 +1412,18 @@ def setUp(self): PendingEmailChangeFactory.create(user=self.test_user) UserOrgTagFactory.create(user=self.test_user, key='foo', value='bar') UserOrgTagFactory.create(user=self.test_user, key='cat', value='dog') + + # Secondary email setup + PendingSecondaryEmailChange.objects.create( + user=self.test_user, + new_secondary_email='pending_secondary@example.com', + activation_key='test_activation_key_123' + ) + AccountRecovery.objects.create( + user=self.test_user, + secondary_email='confirmed_secondary@example.com', + is_active=True + ) CourseEnrollmentAllowedFactory.create(email=self.original_email) @@ -1499,6 +1530,10 @@ def test_retire_user(self, mock_remove_profile_images, mock_get_profile_image_na assert not PendingEmailChange.objects.filter(user=self.test_user).exists() assert not UserOrgTag.objects.filter(user=self.test_user).exists() + + # Verify secondary email models were cleaned + assert not PendingSecondaryEmailChange.objects.filter(user=self.test_user).exists() + assert not AccountRecovery.objects.filter(user=self.test_user).exists() assert not CourseEnrollmentAllowed.objects.filter(email=self.original_email).exists() assert not UnregisteredLearnerCohortAssignments.objects.filter(email=self.original_email).exists() diff --git a/openedx/core/djangoapps/user_api/accounts/utils.py b/openedx/core/djangoapps/user_api/accounts/utils.py index 826dbd42cd13..e525b81be2bb 100644 --- a/openedx/core/djangoapps/user_api/accounts/utils.py +++ b/openedx/core/djangoapps/user_api/accounts/utils.py @@ -15,7 +15,12 @@ from edx_django_utils.user import generate_password from social_django.models import UserSocialAuth -from common.djangoapps.student.models import AccountRecovery, Registration, get_retired_email_by_email +from common.djangoapps.student.models import ( + AccountRecovery, + PendingSecondaryEmailChange, + Registration, + get_retired_email_by_email, +) from openedx.core.djangoapps.site_configuration.models import SiteConfiguration from openedx.core.djangoapps.theming.helpers import get_config_value_from_site_or_settings, get_current_site from openedx.core.djangolib.oauth2_retirement_utils import retire_dot_oauth2_models @@ -219,6 +224,7 @@ def create_retirement_request_and_deactivate_account(user): # Delete OAuth tokens associated with the user. retire_dot_oauth2_models(user) AccountRecovery.retire_recovery_email(user.id) + PendingSecondaryEmailChange.retire_pending_secondary_email(user.id) def username_suffix_generator(suffix_length=4): diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 7260c22a5602..1141543d5e3c 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -39,11 +39,13 @@ from common.djangoapps.track import segment from common.djangoapps.entitlements.models import CourseEntitlement from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=unused-import + AccountRecovery, CourseEnrollmentAllowed, LoginFailures, ManualEnrollmentAudit, PendingEmailChange, PendingNameChange, + PendingSecondaryEmailChange, User, UserProfile, get_potentially_retired_user_by_username, @@ -1099,6 +1101,8 @@ def post(self, request): retirement = UserRetirementStatus.get_retirement_for_retirement_action(username) RevisionPluginRevision.retire_user(retirement.user) ArticleRevision.retire_user(retirement.user) + # Redact PendingNameChange before deletion to prevent plaintext sync to Snowflake + PendingNameChange.objects.filter(user=retirement.user).update(new_name="", rationale="") PendingNameChange.delete_by_user_value(retirement.user, field="user") ManualEnrollmentAudit.retire_manual_enrollments(retirement.user, retirement.retired_email) @@ -1195,8 +1199,15 @@ def post(self, request): self.retire_entitlement_support_detail(user) # Retire misc. models that may contain PII of this user + # Redact pending email change before deletion to prevent plaintext sync to Snowflake + pending_email = PendingEmailChange.objects.filter(user=user).first() + if pending_email: + pending_email.new_email = get_retired_email_by_email(pending_email.new_email) + pending_email.save(update_fields=['new_email']) PendingEmailChange.delete_by_user_value(user, field="user") UserOrgTag.delete_by_user_value(user, field="user") + PendingSecondaryEmailChange.retire_pending_secondary_email(user.id) + AccountRecovery.retire_recovery_email(user.id) # Retire any objects linked to the user via their original email CourseEnrollmentAllowed.delete_by_user_value(original_email, field="email") @@ -1214,6 +1225,7 @@ def post(self, request): user.last_name = "" user.is_active = False user.username = retired_username + user.email = retired_email user.save() except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) diff --git a/openedx/core/djangoapps/user_api/management/commands/retire_user.py b/openedx/core/djangoapps/user_api/management/commands/retire_user.py index f1ef928e2712..7a40dcd84316 100644 --- a/openedx/core/djangoapps/user_api/management/commands/retire_user.py +++ b/openedx/core/djangoapps/user_api/management/commands/retire_user.py @@ -7,7 +7,12 @@ from django.db import transaction from social_django.models import UserSocialAuth -from common.djangoapps.student.models import AccountRecovery, Registration, get_retired_email_by_email +from common.djangoapps.student.models import ( + AccountRecovery, + PendingSecondaryEmailChange, + Registration, + get_retired_email_by_email, +) from openedx.core.djangolib.oauth2_retirement_utils import retire_dot_oauth2_models from ...models import BulkUserRetirementConfig, UserRetirementStatus @@ -158,6 +163,7 @@ def handle(self, *args, **options): # Delete OAuth tokens associated with the user. retire_dot_oauth2_models(user) AccountRecovery.retire_recovery_email(user.id) + PendingSecondaryEmailChange.retire_pending_secondary_email(user.id) except KeyError: error_message = f'Username not specified {user}' logger.error(error_message) diff --git a/openedx/core/djangoapps/user_api/management/tests/test_retire_user.py b/openedx/core/djangoapps/user_api/management/tests/test_retire_user.py index 023efb54f4d7..44be52c0879d 100644 --- a/openedx/core/djangoapps/user_api/management/tests/test_retire_user.py +++ b/openedx/core/djangoapps/user_api/management/tests/test_retire_user.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.management import CommandError, call_command +from common.djangoapps.student.models import PendingSecondaryEmailChange from ...models import UserRetirementStatus from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # lint-amnesty, pylint: disable=unused-import, wrong-import-order setup_retirement_states @@ -105,3 +106,19 @@ def test_retire_with_username_email_userfile(setup_retirement_states): # lint-a with pytest.raises(CommandError, match=r'You cannot use userfile option with username and user_email'): call_command('retire_user', user_file=user_file, username=username, user_email=user_email) remove_user_file() + + +@skip_unless_lms +def test_retire_user_cleans_pending_secondary_email(setup_retirement_states): # lint-amnesty, pylint: disable=redefined-outer-name, unused-argument + user = UserFactory.create(username='user-cleanup', email='user-cleanup@example.com') + PendingSecondaryEmailChange.objects.create( + user=user, + new_secondary_email='pending-secondary@example.com', + activation_key='c' * 32, + ) + + assert PendingSecondaryEmailChange.objects.filter(user=user).exists() + + call_command('retire_user', username=user.username, user_email=user.email) + + assert not PendingSecondaryEmailChange.objects.filter(user=user).exists() From 0d70442174b0c9553df8a57fbf5c00564365a30d Mon Sep 17 00:00:00 2001 From: ktyagiapphelix2u Date: Fri, 10 Apr 2026 08:20:17 +0000 Subject: [PATCH 2/4] fix: retirement PII leaks by redacting pending secondary email/name data --- .../user_api/accounts/tests/test_retirement_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index 38aa857a8850..296dcd46120a 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -1412,7 +1412,7 @@ def setUp(self): PendingEmailChangeFactory.create(user=self.test_user) UserOrgTagFactory.create(user=self.test_user, key='foo', value='bar') UserOrgTagFactory.create(user=self.test_user, key='cat', value='dog') - + # Secondary email setup PendingSecondaryEmailChange.objects.create( user=self.test_user, @@ -1530,7 +1530,7 @@ def test_retire_user(self, mock_remove_profile_images, mock_get_profile_image_na assert not PendingEmailChange.objects.filter(user=self.test_user).exists() assert not UserOrgTag.objects.filter(user=self.test_user).exists() - + # Verify secondary email models were cleaned assert not PendingSecondaryEmailChange.objects.filter(user=self.test_user).exists() assert not AccountRecovery.objects.filter(user=self.test_user).exists() From a0deee3ada5313bc9c810cffd78778463a551682 Mon Sep 17 00:00:00 2001 From: ktyagiapphelix2u Date: Mon, 13 Apr 2026 05:50:37 +0000 Subject: [PATCH 3/4] fix: retirement PII leaks by redacting pending secondary email/name data --- common/djangoapps/student/models/user.py | 2 +- common/djangoapps/student/tests/test_models.py | 8 ++++---- openedx/core/djangoapps/user_api/accounts/utils.py | 2 +- openedx/core/djangoapps/user_api/accounts/views.py | 5 ++--- .../user_api/management/commands/retire_user.py | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 5d16632c9084..5c02a6786a6e 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -942,7 +942,7 @@ class PendingSecondaryEmailChange(DeletableByUserValue, models.Model): activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) @classmethod - def retire_pending_secondary_email(cls, user_id): + def redact_pending_secondary_email(cls, user_id): """ Retire a pending secondary email change row for a user. diff --git a/common/djangoapps/student/tests/test_models.py b/common/djangoapps/student/tests/test_models.py index 81b747cdf14d..b22d0d015d71 100644 --- a/common/djangoapps/student/tests/test_models.py +++ b/common/djangoapps/student/tests/test_models.py @@ -749,7 +749,7 @@ def test_retire_recovery_email(self): class TestPendingSecondaryEmailChange(TestCase): """Tests for retiring PendingSecondaryEmailChange records.""" - def test_retire_pending_secondary_email(self): + def test_redact_pending_secondary_email(self): """Assert that pending secondary email records are deleted for retired users.""" user = UserFactory() PendingSecondaryEmailChange.objects.create( @@ -759,14 +759,14 @@ def test_retire_pending_secondary_email(self): ) assert len(PendingSecondaryEmailChange.objects.filter(user_id=user.id)) == 1 - PendingSecondaryEmailChange.retire_pending_secondary_email(user_id=user.id) + PendingSecondaryEmailChange.redact_pending_secondary_email(user_id=user.id) assert len(PendingSecondaryEmailChange.objects.filter(user_id=user.id)) == 0 - def test_retire_pending_secondary_email_when_no_record(self): + def test_redact_pending_secondary_email_when_no_record(self): """Assert retirement cleanup returns True when no pending secondary row exists.""" user = UserFactory() - assert PendingSecondaryEmailChange.retire_pending_secondary_email(user_id=user.id) is True + assert PendingSecondaryEmailChange.redact_pending_secondary_email(user_id=user.id) is True @ddt.ddt diff --git a/openedx/core/djangoapps/user_api/accounts/utils.py b/openedx/core/djangoapps/user_api/accounts/utils.py index e525b81be2bb..d9d125d27542 100644 --- a/openedx/core/djangoapps/user_api/accounts/utils.py +++ b/openedx/core/djangoapps/user_api/accounts/utils.py @@ -224,7 +224,7 @@ def create_retirement_request_and_deactivate_account(user): # Delete OAuth tokens associated with the user. retire_dot_oauth2_models(user) AccountRecovery.retire_recovery_email(user.id) - PendingSecondaryEmailChange.retire_pending_secondary_email(user.id) + PendingSecondaryEmailChange.redact_pending_secondary_email(user.id) def username_suffix_generator(suffix_length=4): diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 1141543d5e3c..ec2778a02c96 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -1101,8 +1101,7 @@ def post(self, request): retirement = UserRetirementStatus.get_retirement_for_retirement_action(username) RevisionPluginRevision.retire_user(retirement.user) ArticleRevision.retire_user(retirement.user) - # Redact PendingNameChange before deletion to prevent plaintext sync to Snowflake - PendingNameChange.objects.filter(user=retirement.user).update(new_name="", rationale="") + PendingNameChange.objects.filter(user=retirement.user).update(new_name="redacted", rationale="redacted") PendingNameChange.delete_by_user_value(retirement.user, field="user") ManualEnrollmentAudit.retire_manual_enrollments(retirement.user, retirement.retired_email) @@ -1206,7 +1205,7 @@ def post(self, request): pending_email.save(update_fields=['new_email']) PendingEmailChange.delete_by_user_value(user, field="user") UserOrgTag.delete_by_user_value(user, field="user") - PendingSecondaryEmailChange.retire_pending_secondary_email(user.id) + PendingSecondaryEmailChange.redact_pending_secondary_email(user.id) AccountRecovery.retire_recovery_email(user.id) # Retire any objects linked to the user via their original email diff --git a/openedx/core/djangoapps/user_api/management/commands/retire_user.py b/openedx/core/djangoapps/user_api/management/commands/retire_user.py index 7a40dcd84316..67cce6762b39 100644 --- a/openedx/core/djangoapps/user_api/management/commands/retire_user.py +++ b/openedx/core/djangoapps/user_api/management/commands/retire_user.py @@ -163,7 +163,7 @@ def handle(self, *args, **options): # Delete OAuth tokens associated with the user. retire_dot_oauth2_models(user) AccountRecovery.retire_recovery_email(user.id) - PendingSecondaryEmailChange.retire_pending_secondary_email(user.id) + PendingSecondaryEmailChange.redact_pending_secondary_email(user.id) except KeyError: error_message = f'Username not specified {user}' logger.error(error_message) From 59c93085041dc96850c0eb230f9a386c99c8516f Mon Sep 17 00:00:00 2001 From: ktyagiapphelix2u Date: Thu, 16 Apr 2026 10:25:13 +0000 Subject: [PATCH 4/4] fix: retirement PII leaks by redacting pending secondary email/name data --- common/djangoapps/student/models/user.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 5c02a6786a6e..b532e1e39756 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -933,7 +933,7 @@ class PendingSecondaryEmailChange(DeletableByUserValue, models.Model): """ This model keeps track of pending requested changes to a user's secondary email address. - .. pii: Contains new_secondary_email, retired in `DeactivateLogoutView` + .. pii: Contains new_secondary_email, redacted in `DeactivateLogoutView` .. pii_types: email_address .. pii_retirement: local_api """ @@ -944,7 +944,7 @@ class PendingSecondaryEmailChange(DeletableByUserValue, models.Model): @classmethod def redact_pending_secondary_email(cls, user_id): """ - Retire a pending secondary email change row for a user. + Redact a pending secondary email change row for a user. Redacts the email before deletion so any downstream soft-delete mirror does not retain the original secondary email address in the final row image. @@ -954,9 +954,7 @@ def redact_pending_secondary_email(cls, user_id): except cls.DoesNotExist: return True - pending_secondary_email.new_secondary_email = get_retired_email_by_email( - pending_secondary_email.new_secondary_email - ) + pending_secondary_email.new_secondary_email = f"redacted+{user_id}@redacted.com" pending_secondary_email.save(update_fields=['new_secondary_email']) pending_secondary_email.delete() return True @@ -1674,7 +1672,7 @@ 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: the field named secondary_email contains pii, redacted in the `DeactivateLogoutView` .. pii_types: email_address .. pii_retirement: local_api """ @@ -1707,7 +1705,7 @@ def update_recovery_email(self, email): @classmethod def retire_recovery_email(cls, user_id): """ - Retire user's recovery/secondary email as part of GDPR Phase I. + Redact 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 redacted and @@ -1721,7 +1719,7 @@ def retire_recovery_email(cls, user_id): except cls.DoesNotExist: return True - account_recovery.secondary_email = get_retired_email_by_email(account_recovery.secondary_email) + account_recovery.secondary_email = f"redacted+{user_id}@redacted.com" account_recovery.is_active = False account_recovery.save(update_fields=['secondary_email', 'is_active']) account_recovery.delete()