-
Notifications
You must be signed in to change notification settings - Fork 4.3k
fix: redact pending primary email before retirement deletion #38426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -904,14 +904,33 @@ class PendingEmailChange(DeletableByUserValue, models.Model): # noqa: DJ008 | |||||||||||
| """ | ||||||||||||
| This model keeps track of pending requested changes to a user's email address. | ||||||||||||
|
|
||||||||||||
| .. pii: Contains new_email, retired in AccountRetirementView | ||||||||||||
| .. pii: Contains new_email, redacted then deleted in AccountRetirementView | ||||||||||||
| .. pii_types: email_address | ||||||||||||
| .. pii_retirement: local_api | ||||||||||||
| """ | ||||||||||||
| user = models.OneToOneField(User, unique=True, db_index=True, on_delete=models.CASCADE) | ||||||||||||
| new_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 redact_pending_email_by_user_value(cls, value, field): | ||||||||||||
| """ | ||||||||||||
| Redact pending email change fields for records matching ``field=value``. | ||||||||||||
|
|
||||||||||||
| This method is intended for retirement flows where downstream systems | ||||||||||||
| may keep soft-deleted snapshots of these rows. | ||||||||||||
|
|
||||||||||||
| Returns True if redacted, and False if no matching records found. | ||||||||||||
| """ | ||||||||||||
| filter_kwargs = {field: value} | ||||||||||||
| records = list(cls.objects.filter(**filter_kwargs)) | ||||||||||||
| if not records: | ||||||||||||
| return False | ||||||||||||
| for record in records: | ||||||||||||
| record.new_email = get_retired_email_by_email(record.new_email) | ||||||||||||
| record.save(update_fields=['new_email']) | ||||||||||||
|
Comment on lines
+930
to
+931
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description explicitly identifies
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Akanshu-2u The activation_key field has a unique=True database constraint. If we attempt to redact it to a fixed value (empty string or redacted placeholder), we'll violate this constraint when processing multiple users, causing database integrity errors. Additionally, the activation key is a random UUID with no PII - it's just a token
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ktyagiapphelix2u: The PR description mentions the activation key. I agree that this PR should not touch it, but can you update the PR description to remove any mention of it? Thanks. |
||||||||||||
| return True | ||||||||||||
|
|
||||||||||||
| def request_change(self, email): | ||||||||||||
| """Request a change to a user's email. | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| UserAttribute, | ||
| UserCelebration, | ||
| UserProfile, | ||
| get_retired_email_by_email, | ||
| ) | ||
| from common.djangoapps.student.models_api import confirm_name_change, do_name_change_request, get_name | ||
| from common.djangoapps.student.tests.factories import AccountRecoveryFactory, CourseEnrollmentFactory, UserFactory | ||
|
|
@@ -600,6 +601,21 @@ def test_delete_by_user_no_effect_for_user_with_no_email_change(self): | |
| assert not record_was_deleted | ||
| assert 1 == len(PendingEmailChange.objects.all()) | ||
|
|
||
| def test_redact_by_user_redacts_pending_email_change_fields(self): | ||
|
robrap marked this conversation as resolved.
|
||
| original_new_email = self.email_change.new_email | ||
| original_activation_key = self.email_change.activation_key | ||
| expected_retired_email = get_retired_email_by_email(original_new_email) | ||
| record_was_redacted = PendingEmailChange.redact_pending_email_by_user_value(self.user, field='user') | ||
| assert record_was_redacted | ||
| self.email_change.refresh_from_db() | ||
| assert self.email_change.new_email == expected_retired_email | ||
| assert self.email_change.activation_key == original_activation_key | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If activation_key redaction is added, this assertion must be updated to verify the key is also cleared/replaced. As-is, this test will need to change regardless once above issue is fixed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test assertion is correct as-is and does not need to change. As explained in the previous comment thread, we are not adding activation_key redaction |
||
|
|
||
| def test_redact_by_user_no_effect_for_user_with_no_email_change(self): | ||
| """Verify that redacting a user with no pending email change returns False.""" | ||
| record_was_redacted = PendingEmailChange.redact_pending_email_by_user_value(self.user2, field='user') | ||
| assert not record_was_redacted | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really understand this test. Should it just have lines 617 and 618, where you ask to redact on a user that isn't in the table, and it returns that it didn't redact? All the other details about the user 1 email change seem irrelevant and confusing. If you think it is important, I'd need better comments.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test now:
|
||
|
|
||
|
|
||
| class TestCourseEnrollmentAllowed(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1151,7 +1151,10 @@ def post(self, request): | |
|
|
||
| self.retire_entitlement_support_detail(user) | ||
|
|
||
| # Retire misc. models that may contain PII of this user | ||
| # Retire misc. models that may contain PII of this user. | ||
| # Redact pending email before delete because downstream systems | ||
| # may preserve soft-deleted snapshots. | ||
| PendingEmailChange.redact_pending_email_by_user_value(user, field="user") | ||
| PendingEmailChange.delete_by_user_value(user, field="user") | ||
|
Comment on lines
+1157
to
1158
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't we just want to redact in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Additionally, I am alluding to two bugs in my above comment. Are these accurate? The PR description does not yet mention both of these. Also, the ticket has an AC that mentions three bugs. Is there another, or is that a copy/paste issue in the ticket? |
||
| UserOrgTag.delete_by_user_value(user, field="user") | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.