diff --git a/zerver/lib/export.py b/zerver/lib/export.py index 04ff2473bf..6fc227b736 100644 --- a/zerver/lib/export.py +++ b/zerver/lib/export.py @@ -713,9 +713,8 @@ def get_realm_config() -> Config: Config( table="zerver_realmauditlog", - model=RealmAuditLog, - normal_parent=realm_config, - include_rows="realm_id__in", + virtual_parent=realm_config, + custom_fetch=custom_fetch_realm_audit_logs_for_realm, ) Config( @@ -1152,6 +1151,30 @@ def custom_fetch_scheduled_messages(response: TableData, context: Context) -> No response["zerver_scheduledmessage"] = rows +def custom_fetch_realm_audit_logs_for_realm(response: TableData, context: Context) -> None: + """ + Simple custom fetch function to fix up .acting_user for some RealmAuditLog objects. + + Certain RealmAuditLog objects have an acting_user that is in a different .realm, due to + the possibility of server administrators (typically with the .is_staff permission) taking + certain actions to modify UserProfiles or Realms, which will set the .acting_user to + the administrator's UserProfile, which can be in a different realm. Such an acting_user + cannot be imported during organization import on another server, so we need to just set it + to None. + """ + realm = context["realm"] + + query = RealmAuditLog.objects.filter(realm=realm).select_related("acting_user") + realmauditlog_objects = list(query) + for realmauditlog in realmauditlog_objects: + if realmauditlog.acting_user is not None and realmauditlog.acting_user.realm_id != realm.id: + realmauditlog.acting_user = None + + rows = make_raw(realmauditlog_objects) + + response["zerver_realmauditlog"] = rows + + def fetch_usermessages( realm: Realm, message_ids: Set[int], diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index 1f48e1b52a..8d06bd89df 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -83,6 +83,8 @@ from zerver.models import ( get_huddle_hash, get_realm, get_stream, + get_system_bot, + get_user_by_delivery_email, ) @@ -708,6 +710,9 @@ class RealmImportExportTest(ExportFile): cordelia = self.example_user("cordelia") othello = self.example_user("othello") + internal_realm = get_realm(settings.SYSTEM_BOT_REALM) + cross_realm_bot = get_system_bot(settings.WELCOME_BOT, internal_realm.id) + with get_test_image_file("img.png") as img_file: realm_emoji = check_add_realm_emoji( realm=hamlet.realm, name="hawaii", author=hamlet, image_file=img_file @@ -728,6 +733,18 @@ class RealmImportExportTest(ExportFile): original_realm, authentication_methods, acting_user=None ) + # Set up an edge-case RealmAuditLog with acting_user in a different realm. Such an acting_user can't be covered + # by the export, so we'll test that it is handled by getting set to None. + self.assertTrue( + RealmAuditLog.objects.filter( + modified_user=hamlet, event_type=RealmAuditLog.USER_CREATED + ).count(), + 1, + ) + RealmAuditLog.objects.filter( + modified_user=hamlet, event_type=RealmAuditLog.USER_CREATED + ).update(acting_user_id=cross_realm_bot.id) + # data to test import of huddles huddle = [ self.example_user("hamlet"), @@ -989,6 +1006,15 @@ class RealmImportExportTest(ExportFile): imported_realm.authentication_methods_dict(), ) + imported_hamlet = get_user_by_delivery_email(hamlet.delivery_email, imported_realm) + realmauditlog = RealmAuditLog.objects.get( + modified_user=imported_hamlet, event_type=RealmAuditLog.USER_CREATED + ) + self.assertEqual(realmauditlog.realm, imported_realm) + # As explained above when setting up the RealmAuditLog row, the .acting_user should have been + # set to None due to being unexportable. + self.assertEqual(realmauditlog.acting_user, None) + self.assertEqual( Message.objects.filter(realm=original_realm).count(), Message.objects.filter(realm=imported_realm).count(),