-
Notifications
You must be signed in to change notification settings - Fork 4.2k
fix!: redacting user retirement data in lms #37886
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?
fix!: redacting user retirement data in lms #37886
Conversation
70f2265 to
7a7ef1b
Compare
|
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 | |||
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.
Fivetran and snowflake are 2U specific. Can you rewrite this to be more general, and not specific to one instance's tooling?
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.
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") | |||
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.
- looks like this requires a change to the docstring, if you want a second, optional value to be sent.
- 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?
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.
- Updated the docstring. Resolved.
- Changed the suggested things and with run_id it provides traceability to the specific retirement batch/execution.
99b47d4 to
fa6aa32
Compare
|
@pdpinch: @ktyagiapphelix2u is out until Friday, but we will be removing references to 2U and Jenkins. Thanks. |
|
@pdpinch I agreed I am removing jenkins name reference. Thanks. |
| """ | ||
| try: | ||
| usernames = request.data["usernames"] | ||
| redaction_id = request.data.get("redaction_id", "unknown") |
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.
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.
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.
I have implemented the suggested method. You can take a look.
63da9fe to
a7cdaa6
Compare
| """ | ||
| try: | ||
| usernames = request.data["usernames"] | ||
| redacted_value = request.data.get("redacted_value", "redacted") |
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.
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.
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.
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.
19bc46e to
eb5b4df
Compare
| assert retirement.original_username == 'redacted' | ||
| assert retirement.original_email == 'redacted' | ||
| assert retirement.original_name == 'redacted' |
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.
Can we also get a test for non-defaults, to ensure that the right data makes it to the right places?
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.
got it i have added that test for non-defaults.
|
@ktyagiapphelix2u: Also, I think we should change this to a breaking change PR. That means updating the title to 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. |
0c174cf to
536a3bd
Compare
|
@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 |
536a3bd to
5ac51b6
Compare
|
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 |
|
@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. |
|
@ktyagiapphelix2u: Once we ensure we can get everything working with redact + delete, we'd be able to close the DEPR: |
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