Skip to content

Conversation

@ktyagiapphelix2u
Copy link
Contributor

@ktyagiapphelix2u ktyagiapphelix2u commented Jan 14, 2026

Description

Instead of deleting user retirement records completely, this PR updates the system to replace personal information (name, email, username) with safe placeholder values while keeping the records in the database.

Private Ticket

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

@pdpinch
Copy link
Contributor

pdpinch commented Jan 14, 2026

Maybe this is out of scope for this PR, but I'm concerned about the explicit reference to Jenkins. I understand the desirability of recording the job id, but not everyone uses Jenkins as their runner.

@@ -1045,7 +1046,15 @@ def cleanup(self, request):
if len(usernames) != len(retirements):
raise UserRetirementStatus.DoesNotExist("Not all usernames exist in the COMPLETE state.")

retirements.delete()
# Redact PII fields instead of deleting records
# This ensures Fivetran syncs redacted data to Snowflake instead of creating soft deletes with PII
Copy link
Member

Choose a reason for hiding this comment

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

Fivetran and snowflake are 2U specific. Can you rewrite this to be more general, and not specific to one instance's tooling?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rewritten the comments more general. Resolved.

@@ -1032,6 +1032,7 @@ def cleanup(self, request):
"""
try:
usernames = request.data["usernames"]
jenkins_run_id = request.data.get("jenkins_run_id", "unknown")
Copy link
Member

Choose a reason for hiding this comment

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

  1. looks like this requires a change to the docstring, if you want a second, optional value to be sent.
  2. please generalise the name, so it's not about a jenkins job in particular. or better, is there a reason to associate with jenkins? could this just use a value based on random, or just a string (ie. redacted)? Why do we care about associating this with a specific ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Updated the docstring. Resolved.
  2. Changed the suggested things and with run_id it provides traceability to the specific retirement batch/execution.

@robrap
Copy link
Contributor

robrap commented Jan 14, 2026

@pdpinch: @ktyagiapphelix2u is out until Friday, but we will be removing references to 2U and Jenkins. Thanks.

@ktyagiapphelix2u
Copy link
Contributor Author

ktyagiapphelix2u commented Jan 16, 2026

@pdpinch I agreed I am removing jenkins name reference. Thanks.

"""
try:
usernames = request.data["usernames"]
redaction_id = request.data.get("redaction_id", "unknown")
Copy link
Contributor

@robrap robrap Jan 16, 2026

Choose a reason for hiding this comment

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

Can this be 3 inputs: redacted_username, redacted_email, and redacted_name. All decisions of what the redacted value will look like will be made by the caller.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have implemented the suggested method. You can take a look.

"""
try:
usernames = request.data["usernames"]
redacted_value = request.data.get("redacted_value", "redacted")
Copy link
Contributor

Choose a reason for hiding this comment

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

I was asking for 3 different redacted values, like the following:

redacted_username = request.data.get("redacted_username", "redacted")
redacted_email = request.data.get("redacted_email", "redacted")
redacted_name = request.data.get("redacted_name", "redacted")

I know that 2U will send the same value for all 3, but others might want different values.

Copy link
Contributor Author

@ktyagiapphelix2u ktyagiapphelix2u Jan 20, 2026

Choose a reason for hiding this comment

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

got it but i thought these would more cleanest things to do rather than passing all 3 arguements but will make the change accordingly. thanks updating the new stuff.

Comment on lines +1088 to +1090
assert retirement.original_username == 'redacted'
assert retirement.original_email == 'redacted'
assert retirement.original_name == 'redacted'
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also get a test for non-defaults, to ensure that the right data makes it to the right places?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

got it i have added that test for non-defaults.

@robrap
Copy link
Contributor

robrap commented Jan 20, 2026

@ktyagiapphelix2u: Also, I think we should change this to a breaking change PR. That means updating the title to fix!: .... Also, we should open a Fast Track DEPR (i.e automatically accepted) for this as an inform.

We should note that switching from deleting records to redacting records is not a breaking change from the point of view of user retirement, because in either case the sensitive data has been safely taken care of. The breaking change is that these records will no longer be deleted, so any operator scripts that run against the table after retirement, that also relied on the fact that data was being deleted, would need to be updated.

@ktyagiapphelix2u ktyagiapphelix2u changed the title fix: redacting user retirement data in lms fix!: redacting user retirement data in lms Jan 21, 2026
@ktyagiapphelix2u
Copy link
Contributor Author

ktyagiapphelix2u commented Jan 21, 2026

@robrap I was thinking of updating archive cleanup script for Open edX. Is it expected to update it there as well, or should it be left as is? as they have moved to CI workflows rather than jenkins

@bmtcril
Copy link
Contributor

bmtcril commented Jan 21, 2026

Hi all, can we get a description of the problem this is trying to address since we can't see the internal ticket? I have some (very small) amount of concern about this table growing given how often it's called, but more want to understand why this change is necessary.

Since this is a breaking change in the API I think it should be versioned to a V2 endpoint as part of the depr.

@fghaas I know you've been involved in tutor-contrib-retirement so wanted to give you a heads up as well

@robrap
Copy link
Contributor

robrap commented Jan 26, 2026

@bmtcril: In thinking through explaining the issue to you, I may have come up with a backward-compatible version of this change, so that is what I am going to propose we do.

The reason we wanted this change is because in Snowflake, we've used different technologies for sync, but all of them treat the status record deletes as soft-deletes, which then requires an additional custom job to clear out the soft deleted data to fully remove the sensitive data. We decided that redacting here, just like we do everywhere else, would avoid the custom jobs and ongoing maintenance.

I just realized that we could have the best of both worlds, and have this api first redact, and then delete. That way it will still be backward-compatible with the deletes, but we wouldn't have to clean up the soft-deletes, because those records would already be redacted.

@ktyagiapphelix2u: Can you please implement this. You'll need to ensure that the redacted data gets saved to the DB before the delete. I'm not certain exactly how to ensure this, but maybe working with Dave W. to ensure that the soft-deleted records contain the redacted data? Hopefully neither mysql nor Django nor the sync code tries to be too smart.

@robrap
Copy link
Contributor

robrap commented Jan 26, 2026

@ktyagiapphelix2u: Once we ensure we can get everything working with redact + delete, we'd be able to close the DEPR:

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.

6 participants