From eaee5763d69d25caf41429b1e74d02ada24c9fc6 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Thu, 10 Oct 2024 18:08:44 +0530 Subject: [PATCH] realm_export: Add realm_export_consent feature to API. Fixes part of #31201. --- api_docs/changelog.md | 6 +++++ version.py | 2 +- zerver/actions/create_user.py | 12 +++++++++ zerver/actions/user_settings.py | 12 +++++++++ zerver/lib/event_schema.py | 9 +++++++ zerver/lib/events.py | 4 +++ zerver/openapi/zulip.yaml | 35 +++++++++++++++++++++++++ zerver/tests/test_events.py | 45 +++++++++++++++++++++++++++++++++ 8 files changed, 124 insertions(+), 1 deletion(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 110e4f4834..6b5288807a 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 312** + +* [`GET /events`](/api/get-events): Added `realm_export_consent` event + type to allow realm administrators to view which users have + consented to export their private data as part of a realm export. + **Feature level 311** * [`POST /user_groups/{user_group_id}/members`](/api/update-user-group-members): diff --git a/version.py b/version.py index 63b61606be..83d2eedde6 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 311 # Last bumped for updating subgroups. +API_FEATURE_LEVEL = 312 # Last bumped for adding 'realm_export_consent' event type. # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/actions/create_user.py b/zerver/actions/create_user.py index c1be320253..d7a945fc74 100644 --- a/zerver/actions/create_user.py +++ b/zerver/actions/create_user.py @@ -731,6 +731,18 @@ def do_reactivate_user(user_profile: UserProfile, *, acting_user: UserProfile | ) send_event_on_commit(user_profile.realm, event, get_user_ids_who_can_access_user(user_profile)) + if not user_profile.is_bot: + realm_export_consent_event = dict( + type="realm_export_consent", + user_id=user_profile.id, + consented=user_profile.allow_private_data_export, + ) + send_event_on_commit( + user_profile.realm, + realm_export_consent_event, + list(user_profile.realm.get_human_admin_users().values_list("id", flat=True)), + ) + if user_profile.is_bot: event = dict( type="realm_bot", diff --git a/zerver/actions/user_settings.py b/zerver/actions/user_settings.py index 6a78d0c6b8..708cc41967 100644 --- a/zerver/actions/user_settings.py +++ b/zerver/actions/user_settings.py @@ -512,6 +512,18 @@ def do_change_user_setting( send_event_on_commit(user_profile.realm, legacy_event, [user_profile.id]) + if setting_name == "allow_private_data_export": + event = { + "type": "realm_export_consent", + "user_id": user_profile.id, + "consented": setting_value, + } + send_event_on_commit( + user_profile.realm, + event, + list(user_profile.realm.get_human_admin_users().values_list("id", flat=True)), + ) + # Updates to the time zone display setting are sent to all users if setting_name == "timezone": payload = dict( diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index b2628a0995..5297b0566a 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -872,6 +872,15 @@ def check_realm_export( assert has_failed_timestamp == (export["failed_timestamp"] is not None) +realm_export_consent_event = event_dict_type( + [ + ("type", Equals("realm_export_consent")), + ("user_id", int), + ("consented", bool), + ] +) +check_realm_export_consent = make_checker(realm_export_consent_event) + realm_linkifier_type = DictType( required_keys=[ ("pattern", str), diff --git a/zerver/lib/events.py b/zerver/lib/events.py index e2f6dfd81f..2cbe2f9ed4 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -1564,6 +1564,10 @@ def apply_event( # These realm export events are only available to # administrators, and aren't included in page_params. pass + elif event["type"] == "realm_export_consent": + # These 'realm_export_consent' events are only available to + # administrators, and aren't included in page_params. + pass elif event["type"] == "alert_words": state["alert_words"] = event["alert_words"] elif event["type"] == "muted_topics": diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index b4a33031d2..f3b3762193 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3884,6 +3884,41 @@ paths: ], "id": 1, } + - type: object + additionalProperties: false + description: | + Event sent to administrators when the [data export + consent][help-export-consent] status for a user changes, whether due + to a user changing their consent preferences or a user being created + or reactivated (since user creation/activation events do not contain + these data). + + [help-export-consent]: /help/export-your-organization#configure-whether-administrators-can-export-your-private-data + + **Changes**: New in Zulip 10.0 (feature level 312). Previously, + there was not event available to administrators with these data. + properties: + id: + $ref: "#/components/schemas/EventIdSchema" + type: + allOf: + - $ref: "#/components/schemas/EventTypeSchema" + - enum: + - realm_export_consent + user_id: + type: integer + description: | + The ID of the user whose setting was changed. + consented: + type: boolean + description: | + Whether the user has consented for their private data export. + example: + { + "type": "realm_export_consent", + "user_id": 1, + "consented": true, + } - type: object additionalProperties: false description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 663326cc8b..0eb68a205e 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -166,6 +166,7 @@ from zerver.lib.event_schema import ( check_realm_domains_remove, check_realm_emoji_update, check_realm_export, + check_realm_export_consent, check_realm_linkifiers, check_realm_playgrounds, check_realm_update, @@ -3285,6 +3286,17 @@ class NormalActionsTest(BaseAction): check_realm_user_update("events[0]", events[0], "is_active") check_user_group_add_members("events[1]", events[1]) + # Verify that admins receive 'realm_export_consent' event + # when a user is reactivated. + do_deactivate_user(user_profile, acting_user=None) + self.user_profile = self.example_user("iago") + with self.verify_action(num_events=4) as events: + do_reactivate_user(user_profile, acting_user=None) + check_realm_user_update("events[0]", events[0], "is_active") + check_realm_export_consent("events[1]", events[1]) + check_subscription_peer_add("events[2]", events[2]) + check_user_group_add_members("events[3]", events[3]) + def test_do_deactivate_realm(self) -> None: realm = self.user_profile.realm @@ -4248,6 +4260,39 @@ class UserDisplayActionTest(BaseAction): if prop not in UserProfile.notification_setting_types: self.do_change_user_settings_test(prop) + def test_set_allow_private_data_export(self) -> None: + # Verify that both 'user_settings' and 'realm_export_consent' events + # are received by admins when they change the setting. + do_change_user_role( + self.user_profile, UserProfile.ROLE_REALM_ADMINISTRATOR, acting_user=None + ) + self.assertFalse(self.user_profile.allow_private_data_export) + + num_events = 2 + with self.verify_action(num_events=num_events) as events: + do_change_user_setting( + self.user_profile, + "allow_private_data_export", + True, + acting_user=self.user_profile, + ) + check_user_settings_update("events[0]", events[0]) + check_realm_export_consent("events[1]", events[1]) + + # Verify that only 'realm_export_consent' event is received + # by admins when an another user changes their setting. + cordelia = self.example_user("cordelia") + self.assertFalse(cordelia.allow_private_data_export) + num_events = 1 + with self.verify_action(num_events=num_events, state_change_expected=False) as events: + do_change_user_setting( + cordelia, + "allow_private_data_export", + True, + acting_user=cordelia, + ) + check_realm_export_consent("events[0]", events[0]) + def test_set_user_timezone(self) -> None: values = ["America/Denver", "Pacific/Pago_Pago", "Pacific/Galapagos", ""] num_events = 3