Skip to content

Commit ebb2f96

Browse files
fix: Redact SSO PII before deletion
1 parent e3f5a39 commit ebb2f96

2 files changed

Lines changed: 36 additions & 37 deletions

File tree

openedx/core/djangoapps/user_api/accounts/tests/test_utils.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,15 +170,14 @@ def setUp(self):
170170

171171
def _assert_update_before_delete(self, sql_list, table='social_auth_usersocialauth'):
172172
"""
173-
Assert that at least one UPDATE on ``table`` precedes the DELETE in ``sql_list``.
173+
Assert that an UPDATE on ``table`` runs before the DELETE on ``table``.
174174
175-
This verifies that PII was redacted in the database before the row was removed,
176-
which matters because downstream systems may use soft-deletes and could otherwise
177-
retain unredacted data.
175+
This verifies that redaction happens in the helper itself, not via the
176+
fallback pre_delete signal.
178177
"""
179178
table_key = table.upper()
180-
update_indices = [i for i, s in enumerate(sql_list) if 'UPDATE' in s.upper() and table_key in s.upper()]
181-
delete_indices = [i for i, s in enumerate(sql_list) if 'DELETE' in s.upper() and table_key in s.upper()]
179+
update_indices = [i for i, sql in enumerate(sql_list) if 'UPDATE' in sql.upper() and table_key in sql.upper()]
180+
delete_indices = [i for i, sql in enumerate(sql_list) if 'DELETE' in sql.upper() and table_key in sql.upper()]
182181
assert update_indices, f'Expected at least one UPDATE on {table}'
183182
assert delete_indices, f'Expected at least one DELETE on {table}'
184183
assert update_indices[0] < delete_indices[0], 'Expected UPDATE to precede DELETE'
@@ -202,15 +201,15 @@ def create_social_auth(self, provider='google-oauth2', uid='user@example.com', e
202201

203202
def test_redact_and_delete_issues_update_before_delete(self):
204203
"""
205-
Test that redact_and_delete_social_auth issues an UPDATE (redaction) before the DELETE.
204+
Test that redact_and_delete_social_auth redacts before deleting a social auth record.
206205
"""
207206
social_auth = self.create_social_auth()
208207
social_auth_id = social_auth.id
209208

210209
with CaptureQueriesContext(connection) as ctx:
211210
redact_and_delete_social_auth(self.user.id)
212211

213-
self._assert_update_before_delete([q['sql'] for q in ctx])
212+
self._assert_update_before_delete([query['sql'] for query in ctx])
214213
assert not UserSocialAuth.objects.filter(id=social_auth_id).exists()
215214

216215
def test_redact_and_delete_redacts_multiple_sso_providers(self):
@@ -235,6 +234,6 @@ def test_redact_and_delete_redacts_multiple_sso_providers(self):
235234
with CaptureQueriesContext(connection) as ctx:
236235
redact_and_delete_social_auth(self.user.id)
237236

238-
self._assert_update_before_delete([q['sql'] for q in ctx])
237+
self._assert_update_before_delete([query['sql'] for query in ctx])
239238
for auth_id in auth_ids:
240239
assert not UserSocialAuth.objects.filter(id=auth_id).exists()

openedx/core/djangoapps/user_api/management/tests/test_retire_user.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import csv
77
import os
8+
from contextlib import contextmanager
89

910
import pytest
1011
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
@@ -29,6 +30,27 @@
2930
user_file = 'userfile.csv'
3031

3132

33+
@contextmanager
34+
def disconnected_social_auth_redaction_signal():
35+
"""
36+
Temporarily disconnect the fallback signal so these tests exercise the command path.
37+
"""
38+
pre_delete.disconnect(redact_social_auth_pii_before_deletion, sender=UserSocialAuth)
39+
try:
40+
yield
41+
finally:
42+
pre_delete.connect(redact_social_auth_pii_before_deletion, sender=UserSocialAuth)
43+
44+
45+
def assert_update_before_delete(sql_list, table='social_auth_usersocialauth'):
46+
table_key = table.upper()
47+
update_indices = [i for i, sql in enumerate(sql_list) if 'UPDATE' in sql.upper() and table_key in sql.upper()]
48+
delete_indices = [i for i, sql in enumerate(sql_list) if 'DELETE' in sql.upper() and table_key in sql.upper()]
49+
assert update_indices, f'Expected at least one UPDATE on {table}'
50+
assert delete_indices, f'Expected at least one DELETE on {table}'
51+
assert update_indices[0] < delete_indices[0], 'Expected UPDATE to precede DELETE'
52+
53+
3254
def generate_dummy_users():
3355
"""
3456
Function to generate dummy users that needs to be retired
@@ -137,21 +159,10 @@ def test_retire_user_redacts_sso_pii_before_deletion(setup_retirement_states):
137159
)
138160
social_auth_id = social_auth.id
139161

140-
# Disconnect the safety-net signal so we prove retire_user itself redacts first.
141-
pre_delete.disconnect(redact_social_auth_pii_before_deletion, sender=UserSocialAuth)
142-
try:
143-
with CaptureQueriesContext(connection) as ctx:
144-
call_command('retire_user', username=user.username, user_email=user.email)
145-
finally:
146-
pre_delete.connect(redact_social_auth_pii_before_deletion, sender=UserSocialAuth)
162+
with disconnected_social_auth_redaction_signal(), CaptureQueriesContext(connection) as ctx:
163+
call_command('retire_user', username=user.username, user_email=user.email)
147164

148-
sql_list = [q['sql'] for q in ctx]
149-
table_key = 'SOCIAL_AUTH_USERSOCIALAUTH'
150-
update_indices = [i for i, s in enumerate(sql_list) if 'UPDATE' in s.upper() and table_key in s.upper()]
151-
delete_indices = [i for i, s in enumerate(sql_list) if 'DELETE' in s.upper() and table_key in s.upper()]
152-
assert update_indices, 'Expected at least one UPDATE (redaction) on social_auth_usersocialauth'
153-
assert delete_indices, 'Expected at least one DELETE on social_auth_usersocialauth'
154-
assert update_indices[0] < delete_indices[0], 'Expected UPDATE (redaction) to precede DELETE'
165+
assert_update_before_delete([query['sql'] for query in ctx])
155166

156167
assert not UserSocialAuth.objects.filter(id=social_auth_id).exists()
157168

@@ -184,21 +195,10 @@ def test_retire_user_redacts_each_social_auth_before_bulk_deletion(setup_retirem
184195
google_auth_id = google_auth.id
185196
saml_auth_id = saml_auth.id
186197

187-
# Disconnect the safety-net signal so we prove retire_user itself redacts first.
188-
pre_delete.disconnect(redact_social_auth_pii_before_deletion, sender=UserSocialAuth)
189-
try:
190-
with CaptureQueriesContext(connection) as ctx:
191-
call_command('retire_user', username=user.username, user_email=user.email)
192-
finally:
193-
pre_delete.connect(redact_social_auth_pii_before_deletion, sender=UserSocialAuth)
198+
with disconnected_social_auth_redaction_signal(), CaptureQueriesContext(connection) as ctx:
199+
call_command('retire_user', username=user.username, user_email=user.email)
194200

195-
sql_list = [q['sql'] for q in ctx]
196-
table_key = 'SOCIAL_AUTH_USERSOCIALAUTH'
197-
update_indices = [i for i, s in enumerate(sql_list) if 'UPDATE' in s.upper() and table_key in s.upper()]
198-
delete_indices = [i for i, s in enumerate(sql_list) if 'DELETE' in s.upper() and table_key in s.upper()]
199-
assert update_indices, 'Expected at least one UPDATE (redaction) on social_auth_usersocialauth'
200-
assert delete_indices, 'Expected at least one DELETE on social_auth_usersocialauth'
201-
assert update_indices[0] < delete_indices[0], 'Expected UPDATE (redaction) to precede DELETE'
201+
assert_update_before_delete([query['sql'] for query in ctx])
202202

203203
assert not UserSocialAuth.objects.filter(id=google_auth_id).exists()
204204
assert not UserSocialAuth.objects.filter(id=saml_auth_id).exists()

0 commit comments

Comments
 (0)