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.
This commit is contained in:
Prakhar Pratyush 2024-09-17 23:28:09 +05:30 committed by Tim Abbott
parent ffbf2aaaff
commit 86194efe7b
8 changed files with 117 additions and 3 deletions

View File

@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0 ## 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** **Feature level 294**
* [`POST /register`](/api/register-queue): Clients that do not * [`POST /register`](/api/register-queue): Clients that do not

View File

@ -118,6 +118,7 @@
* [Update realm-level defaults of user settings](/api/update-realm-user-settings-defaults) * [Update realm-level defaults of user settings](/api/update-realm-user-settings-defaults)
* [Get all public data exports](/api/get-realm-exports) * [Get all public data exports](/api/get-realm-exports)
* [Create a public data export](/api/export-realm) * [Create a public data export](/api/export-realm)
* [Get data export consent state](/api/get-realm-export-consents)
#### Real-time events #### Real-time events

View File

@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # 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 # 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 # only when going from an old version of the code to a newer version. Bump

View File

@ -694,6 +694,16 @@ def export_realm(client: Client) -> None:
validate_against_openapi_schema(result, "/export/realm", "post", "200") 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") @openapi_test_function("/users/me:get")
def get_profile(client: Client) -> None: def get_profile(client: Client) -> None:
# {code_example|start} # {code_example|start}
@ -1822,6 +1832,7 @@ def test_server_organizations(client: Client) -> None:
create_realm_profile_field(client) create_realm_profile_field(client)
export_realm(client) export_realm(client)
get_realm_exports(client) get_realm_exports(client)
get_realm_export_consents(client)
def test_errors(client: Client) -> None: def test_errors(client: Client) -> None:

View File

@ -12763,6 +12763,57 @@ paths:
description: | description: |
An example JSON error response for when the public data export An example JSON error response for when the public data export
exceeds the maximum allowed data export size. 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: /invites:
get: get:
operationId: get-invites operationId: get-invites

View File

@ -6,6 +6,7 @@ from django.conf import settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from analytics.models import RealmCount from analytics.models import RealmCount
from zerver.actions.user_settings import do_change_user_setting
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.queue import queue_json_publish from zerver.lib.queue import queue_json_publish
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
@ -16,7 +17,7 @@ from zerver.lib.test_helpers import (
stdout_suppressed, stdout_suppressed,
use_s3_backend, 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.models.realm_audit_logs import AuditLogEventType
from zerver.views.realm_export import export_realm from zerver.views.realm_export import export_realm
@ -323,3 +324,30 @@ class RealmExportTest(ZulipTestCase):
result, result,
f"Please request a manual export from {settings.ZULIP_ADMINISTRATOR}.", 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"])

View File

@ -112,3 +112,14 @@ def delete_realm_export(request: HttpRequest, user: UserProfile, export_id: int)
raise JsonableError(_("Export still in progress")) raise JsonableError(_("Export still in progress"))
do_delete_realm_export(user, audit_log_entry) do_delete_realm_export(user, audit_log_entry)
return json_success(request) 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})

View File

@ -119,7 +119,12 @@ from zerver.views.realm_domains import (
patch_realm_domain, patch_realm_domain,
) )
from zerver.views.realm_emoji import delete_emoji, list_emoji, upload_emoji 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_icon import delete_icon_backend, get_icon_backend, upload_icon
from zerver.views.realm_linkifiers import ( from zerver.views.realm_linkifiers import (
create_linkifier, create_linkifier,
@ -504,6 +509,7 @@ v1_api_and_json_patterns = [
# export/realm -> zerver.views.realm_export # export/realm -> zerver.views.realm_export
rest_path("export/realm", POST=export_realm, GET=get_realm_exports), rest_path("export/realm", POST=export_realm, GET=get_realm_exports),
rest_path("export/realm/<int:export_id>", DELETE=delete_realm_export), rest_path("export/realm/<int:export_id>", DELETE=delete_realm_export),
rest_path("export/realm/consents", GET=get_users_export_consents),
] ]
integrations_view = IntegrationView.as_view() integrations_view = IntegrationView.as_view()