-
Notifications
You must be signed in to change notification settings - Fork 11
fix: retirement PII leaks by redacting pending secondary email/name data #227
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: release-ulmo
Are you sure you want to change the base?
Changes from 2 commits
071e3fb
0d70442
a0deee3
59c9308
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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` | ||||||
|
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 would reserve the word "retired" to mean "redacted and can't re-use". Here, I would instead use the word "redacted". |
||||||
| .. 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): | ||||||
|
Member
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 , you may want to rename this function to redact_pending_secondary_email instead? |
||||||
| """ | ||||||
| Retire a pending secondary email change row for a user. | ||||||
|
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. Please search all your changes for "retire" and see if there is anything else that should be changed like this:
Suggested change
|
||||||
|
|
||||||
| 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) | ||||||
|
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. What does Would be good enough to use a hard-coded value like 'redacted@redacted.com`? Reminder that this record is about to be deleted, so this value should mostly never be seen. We just want it to be a valid email format in case the DB would reject an empty string, etc. |
||||||
| account_recovery.is_active = False | ||||||
| account_recovery.save(update_fields=['secondary_email', 'is_active']) | ||||||
| account_recovery.delete() | ||||||
|
|
||||||
| return True | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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="") | ||
|
Member
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, Do we not want to redact the field instead of making this empty string?
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. Yes I have updated the code with redaction |
||
| 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() | ||
|
ktyagiapphelix2u marked this conversation as resolved.
|
||
| except UserRetirementStatus.DoesNotExist: | ||
| return Response(status=status.HTTP_404_NOT_FOUND) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.