diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index 4f942417ae0e..6408f100b5c2 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -1080,7 +1080,27 @@ def cleanup_and_assert_status(self, data=None, expected_status=status.HTTP_204_N def test_simple_success(self): self.cleanup_and_assert_status() - assert not UserRetirementStatus.objects.all() + # Records should be deleted after redaction + retirements = UserRetirementStatus.objects.all() + assert retirements.count() == 0 + + def test_custom_redacted_values(self): + """Test that custom redacted values are applied before deletion.""" + custom_username = 'username-redacted-12345' + custom_email = 'email-redacted-67890' + custom_name = 'name-redacted-abcde' + + data = { + 'usernames': self.usernames, + 'redacted_username': custom_username, + 'redacted_email': custom_email, + 'redacted_name': custom_name + } + self.cleanup_and_assert_status(data=data) + + # Records should be deleted after redaction + retirements = UserRetirementStatus.objects.all() + assert retirements.count() == 0 def test_leaves_other_users(self): remaining_usernames = [] diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index c3ff6ce7a2f2..189ce11cf155 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -1024,14 +1024,20 @@ def cleanup(self, request): ``` { - 'usernames': ['user1', 'user2', ...] + 'usernames': ['user1', 'user2', ...], + 'redacted_username': 'Value to store in username field', + 'redacted_email': 'Value to store in email field', + 'redacted_name': 'Value to store in name field' } ``` - Deletes a batch of retirement requests by username. + Redacts a batch of retirement requests by redacting PII fields. """ try: usernames = request.data["usernames"] + redacted_username = request.data.get("redacted_username", "redacted") + redacted_email = request.data.get("redacted_email", "redacted") + redacted_name = request.data.get("redacted_name", "redacted") if not isinstance(usernames, list): raise TypeError("Usernames should be an array.") @@ -1045,7 +1051,17 @@ 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 first, then delete. This ensures that when Fivetran syncs + # the delete as a soft-delete to the data warehouse, the record will already + # contain redacted values instead of sensitive PII, eliminating the need for + # custom data warehouse cleanup jobs. + for retirement in retirements: + retirement.original_username = redacted_username + retirement.original_email = redacted_email + retirement.original_name = redacted_name + retirement.save() + retirement.delete() + return Response(status=status.HTTP_204_NO_CONTENT) except (RetirementStateError, UserRetirementStatus.DoesNotExist, TypeError) as exc: return Response(str(exc), status=status.HTTP_400_BAD_REQUEST)