Skip to content

fix: redact pending primary email before retirement delete#38385

Open
ktyagiapphelix2u wants to merge 2 commits intoopenedx:masterfrom
ktyagiapphelix2u:ktyagi/primary1
Open

fix: redact pending primary email before retirement delete#38385
ktyagiapphelix2u wants to merge 2 commits intoopenedx:masterfrom
ktyagiapphelix2u:ktyagi/primary1

Conversation

@ktyagiapphelix2u
Copy link
Copy Markdown
Contributor

@ktyagiapphelix2u ktyagiapphelix2u commented Apr 20, 2026

Summary

This change resolves a privacy gap in the retirement flow for users with a pending primary email change. It ensures sensitive fields are redacted before deletion so no readable data persists after retirement.

Problem

When a user retires while having an active record in student_pendingemailchange, the record is deleted as part of the retirement process. However, due to downstream soft-delete behavior, the deleted record may still persist with sensitive fields (such as pending email and activation key) in a readable form.

Root Cause

The retirement flow was directly deleting PendingEmailChange records without redacting sensitive fields beforehand.

What Changed

  • Introduced a helper method to redact sensitive fields in PendingEmailChange before deletion
  • Updated the retirement flow to enforce a redact-then-delete sequence
  • Added tests to validate correct redaction and execution order
  • Updated inline documentation to clearly describe the redact-before-delete behavior

Behavior Before

  • User retires with a pending primary email change
  • System deletes the pending record
  • Soft-deleted record may still contain raw sensitive values

Behavior After

  • User retires with a pending primary email change
  • System first redacts sensitive fields in the record
  • Record is then deleted
  • Any retained soft-deleted record contains only redacted values

Ticket & Reference

https://2u-internal.atlassian.net/browse/BOMS-499

@ktyagiapphelix2u ktyagiapphelix2u requested a review from a team as a code owner April 20, 2026 08:43
Copy link
Copy Markdown
Contributor

@robrap robrap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not records_matching_user_value.exists():
return False
for record in records_matching_user_value:
record.new_email = get_retired_email_by_email(record.new_email)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we discuss get_retired_email_by_email on another PR already? Where is that comment?

return False
for record in records_matching_user_value:
record.new_email = get_retired_email_by_email(record.new_email)
record.activation_key = uuid.uuid4().hex
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this done?

# Retire misc. models that may contain PII of this user.
# Redact pending primary email fields before delete because
# downstream replication can preserve soft-deleted snapshots.
PendingEmailChange.redact_pending_email_by_user_value(user, field="user")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on delete_by_user_value, it seems like redact_by_user_value would be a more consistent name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants