export: Add guardrails against generating a dysfunctional export via UI.

As explained in the comments, if in an export with consent there are no
consenting owners or in a public export there are no owners with email
visibility set to at least ADMINS, the exported data will, upon import,
create an organization without usable owner accounts.
This commit is contained in:
Mateusz Mandera
2025-03-19 03:16:53 +08:00
committed by Tim Abbott
parent 9864eee029
commit 8ab400b95d
2 changed files with 76 additions and 3 deletions

View File

@@ -208,6 +208,41 @@ class RealmExportTest(ZulipTestCase):
admin = self.example_user("iago")
self.login_user(admin)
owner = self.example_user("desdemona")
do_change_user_setting(
owner,
"email_address_visibility",
UserProfile.EMAIL_ADDRESS_VISIBILITY_NOBODY,
acting_user=None,
)
result = self.client_post("/json/export/realm")
self.assert_json_error_contains(
result,
"Make sure at least one Organization Owner allows "
"other Administrators to see their email address",
)
do_change_user_setting(
owner,
"email_address_visibility",
UserProfile.EMAIL_ADDRESS_VISIBILITY_ADMINS,
acting_user=None,
)
do_change_user_setting(
owner,
"allow_private_data_export",
False,
acting_user=None,
)
result = self.client_post(
"/json/export/realm",
info={"export_type": RealmExport.EXPORT_FULL_WITH_CONSENT},
)
self.assert_json_error_contains(
result, "Make sure at least one Organization Owner is consenting"
)
with (
patch(
"zerver.lib.export.do_export_realm", side_effect=Exception("failure")

View File

@@ -1,7 +1,6 @@
from datetime import timedelta
from typing import Annotated
from django.conf import settings
from django.db import transaction
from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now as timezone_now
@@ -12,9 +11,10 @@ from analytics.models import RealmCount
from zerver.actions.realm_export import do_delete_realm_export, notify_realm_export
from zerver.decorator import require_realm_admin
from zerver.lib.exceptions import JsonableError
from zerver.lib.export import get_realm_exports_serialized
from zerver.lib.export import get_consented_user_ids, get_realm_exports_serialized
from zerver.lib.queue import queue_event_on_commit
from zerver.lib.response import json_success
from zerver.lib.send_email import FromAddress
from zerver.lib.typed_endpoint import typed_endpoint
from zerver.lib.typed_endpoint_validators import check_int_in_validator
from zerver.models import RealmExport, UserProfile
@@ -78,10 +78,48 @@ def export_realm(
):
raise JsonableError(
_("Please request a manual export from {email}.").format(
email=settings.ZULIP_ADMINISTRATOR,
email=FromAddress.SUPPORT,
)
)
if export_type == RealmExport.EXPORT_FULL_WITH_CONSENT:
# Users without consent enabled will end up deactivated in the exported
# data. An organization without a consenting Owner would therefore not be
# functional after export->import. That's most likely not desired by the user
# so check for such a case and return an error.
consented_user_ids = get_consented_user_ids(realm)
if not UserProfile.objects.filter(
id__in=consented_user_ids, role=UserProfile.ROLE_REALM_OWNER, realm=realm
).exists():
raise JsonableError(
_(
"Make sure at least one Organization Owner is consenting to the export "
"or contact {email} for help."
).format(email=FromAddress.SUPPORT)
)
elif export_type == RealmExport.EXPORT_PUBLIC:
# Since users with email visibility set to NOBODY won't have their real emails
# exported, this could result in a lack of functional Owner accounts.
# We make sure that at least one Owner can have their real email address exported
# or return an error.
if not UserProfile.objects.filter(
role=UserProfile.ROLE_REALM_OWNER,
email_address_visibility__in=[
UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE,
UserProfile.EMAIL_ADDRESS_VISIBILITY_MEMBERS,
UserProfile.EMAIL_ADDRESS_VISIBILITY_MODERATORS,
UserProfile.EMAIL_ADDRESS_VISIBILITY_ADMINS,
],
realm=realm,
).exists():
raise JsonableError(
_(
"Make sure at least one Organization Owner allows other "
"Administrators to see their email address "
"or contact {email} for help"
).format(email=FromAddress.SUPPORT)
)
row = RealmExport.objects.create(
realm=realm,
type=export_type,