This repository has been archived by the owner on Apr 3, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #63 from openedx/jill/retire-user
Adds user retirement sink
- Loading branch information
Showing
12 changed files
with
253 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,4 @@ | |
A sink for Open edX events to send them to ClickHouse. | ||
""" | ||
|
||
__version__ = "0.4.0" | ||
__version__ = "0.5.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""User retirement sink""" | ||
import requests | ||
from django.conf import settings | ||
|
||
from event_sink_clickhouse.serializers import UserRetirementSerializer | ||
from event_sink_clickhouse.sinks.base_sink import ModelBaseSink | ||
|
||
|
||
class UserRetirementSink(ModelBaseSink): # pylint: disable=abstract-method | ||
""" | ||
Sink for user retirement events | ||
""" | ||
|
||
model = "auth_user" | ||
unique_key = "id" | ||
clickhouse_table_name = ( | ||
"dummy" # uses settings.EVENT_SINK_CLICKHOUSE_PII_MODELS instead | ||
) | ||
timestamp_field = "modified" | ||
name = "User Retirement" | ||
serializer_class = UserRetirementSerializer | ||
|
||
def send_item(self, serialized_item, many=False): | ||
""" | ||
Unlike the other data sinks, the User Retirement sink deletes records from the user PII tables in Clickhouse. | ||
Send delete queries to remove the serialized User from ClickHouse. | ||
""" | ||
if many: | ||
users = serialized_item | ||
else: | ||
users = [serialized_item] | ||
user_ids = {str(user["user_id"]) for user in users} | ||
user_ids_str = ",".join(sorted(user_ids)) | ||
clickhouse_pii_tables = getattr( | ||
settings, "EVENT_SINK_CLICKHOUSE_PII_MODELS", [] | ||
) | ||
|
||
for table in clickhouse_pii_tables: | ||
params = { | ||
"query": f"ALTER TABLE {self.ch_database}.{table} DELETE WHERE user_id in ({user_ids_str})", | ||
} | ||
request = requests.Request( | ||
"POST", | ||
self.ch_url, | ||
params=params, | ||
auth=self.ch_auth, | ||
) | ||
self._send_clickhouse_request( | ||
request, | ||
expected_insert_rows=0, # DELETE requests don't return a row count | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ | |
|
||
diff-cover # Changeset diff test coverage | ||
edx-i18n-tools # For i18n_tool dummy | ||
black # For formatting |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,8 @@ | |
"language", | ||
]) | ||
|
||
FakeUser = namedtuple("FakeUser", ["id"]) | ||
|
||
|
||
class FakeXBlock: | ||
""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
""" | ||
Tests for the user_retire sinks. | ||
""" | ||
import logging | ||
from unittest.mock import patch | ||
|
||
import responses | ||
from django.test.utils import override_settings | ||
from responses.registries import OrderedRegistry | ||
|
||
from event_sink_clickhouse.sinks.user_retire import UserRetirementSink | ||
from event_sink_clickhouse.tasks import dump_data_to_clickhouse | ||
from test_utils.helpers import FakeUser | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
@responses.activate( # pylint: disable=unexpected-keyword-arg,no-value-for-parameter | ||
registry=OrderedRegistry | ||
) | ||
@override_settings(EVENT_SINK_CLICKHOUSE_PII_MODELS=["user_profile", "external_id"]) | ||
@patch("event_sink_clickhouse.sinks.user_retire.UserRetirementSink.serialize_item") | ||
@patch("event_sink_clickhouse.sinks.user_retire.UserRetirementSink.is_enabled") | ||
@patch("event_sink_clickhouse.sinks.user_retire.UserRetirementSink.get_model") | ||
def test_retire_user(mock_user_model, mock_is_enabled, mock_serialize_item): | ||
""" | ||
Test of a successful user retirement. | ||
""" | ||
# Create a fake user | ||
user = FakeUser(246) | ||
mock_user_model.return_value.get_from_id.return_value = user | ||
mock_is_enabled.return_value = True | ||
mock_serialize_item.return_value = {"user_id": user.id} | ||
|
||
# Use the responses library to catch the POSTs to ClickHouse | ||
# and match them against the expected values | ||
user_profile_delete = responses.post( | ||
"https://foo.bar/", | ||
match=[ | ||
responses.matchers.query_param_matcher( | ||
{ | ||
"query": f"ALTER TABLE cool_data.user_profile DELETE WHERE user_id in ({user.id})", | ||
} | ||
) | ||
], | ||
) | ||
external_id_delete = responses.post( | ||
"https://foo.bar/", | ||
match=[ | ||
responses.matchers.query_param_matcher( | ||
{ | ||
"query": f"ALTER TABLE cool_data.external_id DELETE WHERE user_id in ({user.id})", | ||
} | ||
) | ||
], | ||
) | ||
|
||
sink = UserRetirementSink(None, None) | ||
dump_data_to_clickhouse( | ||
sink_module=sink.__module__, | ||
sink_name=sink.__class__.__name__, | ||
object_id=user.id, | ||
) | ||
|
||
assert mock_user_model.call_count == 1 | ||
assert mock_is_enabled.call_count == 1 | ||
assert mock_serialize_item.call_count == 1 | ||
assert user_profile_delete.call_count == 1 | ||
assert external_id_delete.call_count == 1 | ||
|
||
|
||
@responses.activate( # pylint: disable=unexpected-keyword-arg,no-value-for-parameter | ||
registry=OrderedRegistry | ||
) | ||
@override_settings(EVENT_SINK_CLICKHOUSE_PII_MODELS=["user_profile"]) | ||
@patch("event_sink_clickhouse.sinks.user_retire.UserRetirementSink.serialize_item") | ||
def test_retire_many_users(mock_serialize_item): | ||
""" | ||
Test of a successful "many users" retirement. | ||
""" | ||
# Create and serialize a few fake users | ||
users = (FakeUser(246), FakeUser(22), FakeUser(91)) | ||
mock_serialize_item.return_value = [{"user_id": user.id} for user in users] | ||
|
||
# Use the responses library to catch the POSTs to ClickHouse | ||
# and match them against the expected values | ||
user_profile_delete = responses.post( | ||
"https://foo.bar/", | ||
match=[ | ||
responses.matchers.query_param_matcher( | ||
{ | ||
"query": "ALTER TABLE cool_data.user_profile DELETE WHERE user_id in (22,246,91)", | ||
} | ||
) | ||
], | ||
) | ||
|
||
sink = UserRetirementSink(None, log) | ||
sink.dump( | ||
item_id=users[0].id, | ||
many=True, | ||
) | ||
|
||
assert mock_serialize_item.call_count == 1 | ||
assert user_profile_delete.call_count == 1 |