-
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_matching_user_value = cls.objects.filter(**filter_kwargs) | ||||||||||||||||||
| if not records_matching_user_value.exists(): | ||||||||||||||||||
| return False | ||||||||||||||||||
| for record in records_matching_user_value: | ||||||||||||||||||
|
Comment on lines
+926
to
+929
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. Both queries fetch the same data. Since the queryset is lazy,
Suggested change
This is a single DB hit, which matters more if the Note: The change is optional. |
||||||||||||||||||
| 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
|
||||||||||||||||||
| 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): | ||
|
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. Should this be updated to test for multiple pending records, and ensuring that they are all redacted?
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 user field is a OneToOneField with unique=True, so there can only be one PendingEmailChange per user. The test already covers the maximum case (redacting 1 record). Multiple records per user aren't possible with this model constraint. |
||
| 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. |
||
|
|
||
| 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 | ||
|
|
||
|
|
||
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.