From 86194efe7bde0d734b34105884cbe41887d3826a Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Tue, 17 Sep 2024 23:28:09 +0530 Subject: [PATCH] realm_export: Add a new endpoint to fetch private data export consents. This commit adds a new endpoint `export/realm/consents` to fetch the consents of users for their private data exports. Fixes part of #31201. --- api_docs/changelog.md | 6 ++++ api_docs/include/rest-endpoints.md | 1 + version.py | 2 +- zerver/openapi/python_examples.py | 11 +++++++ zerver/openapi/zulip.yaml | 51 ++++++++++++++++++++++++++++++ zerver/tests/test_realm_export.py | 30 +++++++++++++++++- zerver/views/realm_export.py | 11 +++++++ zproject/urls.py | 8 ++++- 8 files changed, 117 insertions(+), 3 deletions(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 4261dc6115..638d932485 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 295** + +* [`GET /export/realm/consents`](/api/get-realm-export-consents): Added + a new endpoint to fetch the consents of users for their [private data + exports](/help/export-your-organization#full-export-with-member-consent). + **Feature level 294** * [`POST /register`](/api/register-queue): Clients that do not diff --git a/api_docs/include/rest-endpoints.md b/api_docs/include/rest-endpoints.md index de65162f76..e5538ec957 100644 --- a/api_docs/include/rest-endpoints.md +++ b/api_docs/include/rest-endpoints.md @@ -118,6 +118,7 @@ * [Update realm-level defaults of user settings](/api/update-realm-user-settings-defaults) * [Get all public data exports](/api/get-realm-exports) * [Create a public data export](/api/export-realm) +* [Get data export consent state](/api/get-realm-export-consents) #### Real-time events diff --git a/version.py b/version.py index 48c6e70de0..6e1d7940dd 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 = 294 # Last bumped for `include_daectivated_groups` client capability. +API_FEATURE_LEVEL = 295 # Last bumped for `/export/realm/consents` endpoint. # 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/openapi/python_examples.py b/zerver/openapi/python_examples.py index da74c9e540..e09921196a 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -694,6 +694,16 @@ def export_realm(client: Client) -> None: validate_against_openapi_schema(result, "/export/realm", "post", "200") +@openapi_test_function("/export/realm/consents:get") +def get_realm_export_consents(client: Client) -> None: + # {code_example|start} + # Get the consents of users for their private data exports. + result = client.call_endpoint(url="/export/realm/consents", method="GET") + # {code_example|end} + assert_success_response(result) + validate_against_openapi_schema(result, "/export/realm/consents", "get", "200") + + @openapi_test_function("/users/me:get") def get_profile(client: Client) -> None: # {code_example|start} @@ -1822,6 +1832,7 @@ def test_server_organizations(client: Client) -> None: create_realm_profile_field(client) export_realm(client) get_realm_exports(client) + get_realm_export_consents(client) def test_errors(client: Client) -> None: diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index fbe16e1523..30507614c2 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -12763,6 +12763,57 @@ paths: description: | An example JSON error response for when the public data export exceeds the maximum allowed data export size. + /export/realm/consents: + get: + operationId: get-realm-export-consents + summary: Get data export consent state + tags: ["server_and_organizations"] + x-requires-administrator: true + description: | + Fetches which users have [consented](/help/export-your-organization#full-export-with-member-consent) + for their private data to be exported by organization administrators. + + **Changes**: New in Zulip 10.0 (feature level 295). + responses: + "200": + description: Success. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - additionalProperties: false + properties: + result: {} + msg: {} + ignored_parameters_unsupported: {} + export_consents: + type: array + description: | + An array of objects where each object contains a user ID and + whether the user has consented for their private data to be exported. + items: + type: object + additionalProperties: false + properties: + user_id: + description: | + The user ID. + type: integer + consented: + description: | + Whether the user has consented for their private data export. + type: boolean + example: + { + "export_consents": + [ + {"user_id": 11, "consented": true}, + {"user_id": 6, "consented": false}, + ], + "msg": "", + "result": "success", + } /invites: get: operationId: get-invites diff --git a/zerver/tests/test_realm_export.py b/zerver/tests/test_realm_export.py index f94b29df04..7a6afb376c 100644 --- a/zerver/tests/test_realm_export.py +++ b/zerver/tests/test_realm_export.py @@ -6,6 +6,7 @@ from django.conf import settings from django.utils.timezone import now as timezone_now from analytics.models import RealmCount +from zerver.actions.user_settings import do_change_user_setting from zerver.lib.exceptions import JsonableError from zerver.lib.queue import queue_json_publish from zerver.lib.test_classes import ZulipTestCase @@ -16,7 +17,7 @@ from zerver.lib.test_helpers import ( stdout_suppressed, use_s3_backend, ) -from zerver.models import Realm, RealmAuditLog +from zerver.models import Realm, RealmAuditLog, UserProfile from zerver.models.realm_audit_logs import AuditLogEventType from zerver.views.realm_export import export_realm @@ -323,3 +324,30 @@ class RealmExportTest(ZulipTestCase): result, f"Please request a manual export from {settings.ZULIP_ADMINISTRATOR}.", ) + + def test_get_users_export_consents(self) -> None: + admin = self.example_user("iago") + self.login_user(admin) + + # By default, export consent is set to False. + self.assertFalse( + UserProfile.objects.filter( + realm=admin.realm, is_active=True, is_bot=False, allow_private_data_export=True + ).exists() + ) + + # Hamlet and Aaron consented to export their private data. + hamlet = self.example_user("hamlet") + aaron = self.example_user("aaron") + for user in [hamlet, aaron]: + do_change_user_setting(user, "allow_private_data_export", True, acting_user=None) + + # Verify export consents of users. + result = self.client_get("/json/export/realm/consents") + response_dict = self.assert_json_success(result) + export_consents = response_dict["export_consents"] + for export_consent in export_consents: + if export_consent["user_id"] in [hamlet.id, aaron.id]: + self.assertTrue(export_consent["consented"]) + continue + self.assertFalse(export_consent["consented"]) diff --git a/zerver/views/realm_export.py b/zerver/views/realm_export.py index e7f5679fca..4faa73b54c 100644 --- a/zerver/views/realm_export.py +++ b/zerver/views/realm_export.py @@ -112,3 +112,14 @@ def delete_realm_export(request: HttpRequest, user: UserProfile, export_id: int) raise JsonableError(_("Export still in progress")) do_delete_realm_export(user, audit_log_entry) return json_success(request) + + +@require_realm_admin +def get_users_export_consents(request: HttpRequest, user: UserProfile) -> HttpResponse: + rows = UserProfile.objects.filter(realm=user.realm, is_active=True, is_bot=False).values( + "id", "allow_private_data_export" + ) + export_consents = [ + {"user_id": row["id"], "consented": row["allow_private_data_export"]} for row in rows + ] + return json_success(request, data={"export_consents": export_consents}) diff --git a/zproject/urls.py b/zproject/urls.py index 4d88085fe1..3a5f6450fd 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -119,7 +119,12 @@ from zerver.views.realm_domains import ( patch_realm_domain, ) from zerver.views.realm_emoji import delete_emoji, list_emoji, upload_emoji -from zerver.views.realm_export import delete_realm_export, export_realm, get_realm_exports +from zerver.views.realm_export import ( + delete_realm_export, + export_realm, + get_realm_exports, + get_users_export_consents, +) from zerver.views.realm_icon import delete_icon_backend, get_icon_backend, upload_icon from zerver.views.realm_linkifiers import ( create_linkifier, @@ -504,6 +509,7 @@ v1_api_and_json_patterns = [ # export/realm -> zerver.views.realm_export rest_path("export/realm", POST=export_realm, GET=get_realm_exports), rest_path("export/realm/", DELETE=delete_realm_export), + rest_path("export/realm/consents", GET=get_users_export_consents), ] integrations_view = IntegrationView.as_view()