mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	events: Rewrite system for managing realm exports.
This feature is intended to cover all of our ways of exporting a realm, not just the initial "public export" feature, so we should name things appropriately for that goal. Additionally, we don't want to include data exports in page_params; the original implementation was actually buggy and would have.
This commit is contained in:
		@@ -38,6 +38,7 @@ from zerver.lib.email_mirror_helpers import encode_email_address, encode_email_a
 | 
				
			|||||||
from zerver.lib.emoji import emoji_name_to_emoji_code, get_emoji_file_name
 | 
					from zerver.lib.emoji import emoji_name_to_emoji_code, get_emoji_file_name
 | 
				
			||||||
from zerver.lib.exceptions import StreamDoesNotExistError, \
 | 
					from zerver.lib.exceptions import StreamDoesNotExistError, \
 | 
				
			||||||
    StreamWithIDDoesNotExistError
 | 
					    StreamWithIDDoesNotExistError
 | 
				
			||||||
 | 
					from zerver.lib.export import get_realm_exports_serialized
 | 
				
			||||||
from zerver.lib.hotspots import get_next_hotspots
 | 
					from zerver.lib.hotspots import get_next_hotspots
 | 
				
			||||||
from zerver.lib.message import (
 | 
					from zerver.lib.message import (
 | 
				
			||||||
    access_message,
 | 
					    access_message,
 | 
				
			||||||
@@ -5670,8 +5671,8 @@ def get_zoom_video_call_url(realm: Realm) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return response['join_url']
 | 
					    return response['join_url']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def notify_export_completed(user_profile: UserProfile, public_url: str) -> None:
 | 
					def notify_export_completed(user_profile: UserProfile) -> None:
 | 
				
			||||||
    # In the future, we may want to send this event to all realm admins.
 | 
					    # In the future, we may want to send this event to all realm admins.
 | 
				
			||||||
    event = dict(type='realm_exported',
 | 
					    event = dict(type='realm_export',
 | 
				
			||||||
                 public_url=public_url)
 | 
					                 exports=get_realm_exports_serialized(user_profile))
 | 
				
			||||||
    send_event(user_profile.realm, event, [user_profile.id])
 | 
					    send_event(user_profile.realm, event, [user_profile.id])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -751,6 +751,10 @@ def apply_event(state: Dict[str, Any],
 | 
				
			|||||||
                                      if realm_domain['domain'] != event['domain']]
 | 
					                                      if realm_domain['domain'] != event['domain']]
 | 
				
			||||||
    elif event['type'] == "realm_emoji":
 | 
					    elif event['type'] == "realm_emoji":
 | 
				
			||||||
        state['realm_emoji'] = event['realm_emoji']
 | 
					        state['realm_emoji'] = event['realm_emoji']
 | 
				
			||||||
 | 
					    elif event['type'] == 'realm_export':
 | 
				
			||||||
 | 
					        # These realm export events are only available to
 | 
				
			||||||
 | 
					        # administrators, and aren't included in page_params.
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
    elif event['type'] == "alert_words":
 | 
					    elif event['type'] == "alert_words":
 | 
				
			||||||
        state['alert_words'] = event['alert_words']
 | 
					        state['alert_words'] = event['alert_words']
 | 
				
			||||||
    elif event['type'] == "muted_topics":
 | 
					    elif event['type'] == "muted_topics":
 | 
				
			||||||
@@ -812,8 +816,6 @@ def apply_event(state: Dict[str, Any],
 | 
				
			|||||||
            user_status.pop(user_id, None)
 | 
					            user_status.pop(user_id, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        state['user_status'] = user_status
 | 
					        state['user_status'] = user_status
 | 
				
			||||||
    elif event['type'] == 'realm_exported':
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        raise AssertionError("Unexpected event type %s" % (event['type'],))
 | 
					        raise AssertionError("Unexpected event type %s" % (event['type'],))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1702,7 +1702,7 @@ def export_realm_wrapper(realm: Realm, output_dir: str,
 | 
				
			|||||||
        print("Successfully deleted the tarball at %s" % (tarball_path,))
 | 
					        print("Successfully deleted the tarball at %s" % (tarball_path,))
 | 
				
			||||||
    return public_url
 | 
					    return public_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_public_exports_serialized(user: UserProfile) -> List[Dict[str, Any]]:
 | 
					def get_realm_exports_serialized(user: UserProfile) -> List[Dict[str, Any]]:
 | 
				
			||||||
    all_exports = RealmAuditLog.objects.filter(realm=user.realm,
 | 
					    all_exports = RealmAuditLog.objects.filter(realm=user.realm,
 | 
				
			||||||
                                               event_type=RealmAuditLog.REALM_EXPORTED)
 | 
					                                               event_type=RealmAuditLog.REALM_EXPORTED)
 | 
				
			||||||
    exports_dict = {}
 | 
					    exports_dict = {}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2759,19 +2759,26 @@ class EventsRegisterTest(ZulipTestCase):
 | 
				
			|||||||
        error = schema_checker('events[0]', events[0])
 | 
					        error = schema_checker('events[0]', events[0])
 | 
				
			||||||
        self.assert_on_error(error)
 | 
					        self.assert_on_error(error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_public_export_notify_admins(self) -> None:
 | 
					    def test_realm_export_notify_admins(self) -> None:
 | 
				
			||||||
 | 
					        # TODO: This test is completely busted because the
 | 
				
			||||||
 | 
					        # RealmAuditLog table is empty in it, so it's testing an event
 | 
				
			||||||
 | 
					        # containing an empty list.
 | 
				
			||||||
        schema_checker = self.check_events_dict([
 | 
					        schema_checker = self.check_events_dict([
 | 
				
			||||||
            ('type', equals('realm_exported')),
 | 
					            ('type', equals('realm_export')),
 | 
				
			||||||
            ('public_url', check_string),
 | 
					            ('exports', check_list(check_dict_only([
 | 
				
			||||||
 | 
					                ('id', check_string),
 | 
				
			||||||
 | 
					                ('event_time', check_string),
 | 
				
			||||||
 | 
					                ('acting_user_id', check_int),
 | 
				
			||||||
 | 
					                ('path', check_string),
 | 
				
			||||||
 | 
					            ])))
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Traditionally, we'd be testing the endpoint, but that
 | 
					        # Traditionally, we'd be testing the endpoint, but that
 | 
				
			||||||
        # requires somewhat annoying mocking setup for what to do with
 | 
					        # requires somewhat annoying mocking setup for what to do with
 | 
				
			||||||
        # the export tarball.
 | 
					        # the export tarball.
 | 
				
			||||||
        events = self.do_test(
 | 
					        events = self.do_test(
 | 
				
			||||||
            lambda: notify_export_completed(self.user_profile,
 | 
					            lambda: notify_export_completed(self.user_profile),
 | 
				
			||||||
                                            "http://localhost:9991/path/to/export.tar.gz"),
 | 
					            state_change_expected=False, num_events=1)
 | 
				
			||||||
            state_change_expected=False)
 | 
					 | 
				
			||||||
        error = schema_checker('events[0]', events[0])
 | 
					        error = schema_checker('events[0]', events[0])
 | 
				
			||||||
        self.assert_on_error(error)
 | 
					        self.assert_on_error(error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ from zerver.lib.exceptions import JsonableError
 | 
				
			|||||||
from zerver.lib.test_helpers import use_s3_backend, create_s3_buckets
 | 
					from zerver.lib.test_helpers import use_s3_backend, create_s3_buckets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zerver.models import RealmAuditLog
 | 
					from zerver.models import RealmAuditLog
 | 
				
			||||||
from zerver.views.public_export import public_only_realm_export
 | 
					from zerver.views.realm_export import export_realm
 | 
				
			||||||
import zerver.lib.upload
 | 
					import zerver.lib.upload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
@@ -24,7 +24,7 @@ class RealmExportTest(ZulipTestCase):
 | 
				
			|||||||
        user = self.example_user('hamlet')
 | 
					        user = self.example_user('hamlet')
 | 
				
			||||||
        self.login(user.email)
 | 
					        self.login(user.email)
 | 
				
			||||||
        with self.assertRaises(JsonableError):
 | 
					        with self.assertRaises(JsonableError):
 | 
				
			||||||
            public_only_realm_export(self.client_post, user)
 | 
					            export_realm(self.client_post, user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @use_s3_backend
 | 
					    @use_s3_backend
 | 
				
			||||||
    def test_endpoint_s3(self) -> None:
 | 
					    def test_endpoint_s3(self) -> None:
 | 
				
			||||||
@@ -57,7 +57,7 @@ class RealmExportTest(ZulipTestCase):
 | 
				
			|||||||
        result = self.client_get('/json/export/realm')
 | 
					        result = self.client_get('/json/export/realm')
 | 
				
			||||||
        self.assert_json_success(result)
 | 
					        self.assert_json_success(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        export_dict = result.json()['public_exports']
 | 
					        export_dict = result.json()['exports']
 | 
				
			||||||
        self.assertEqual(export_dict[0]['path'], path_id)
 | 
					        self.assertEqual(export_dict[0]['path'], path_id)
 | 
				
			||||||
        self.assertEqual(export_dict[0]['acting_user_id'], admin.id)
 | 
					        self.assertEqual(export_dict[0]['acting_user_id'], admin.id)
 | 
				
			||||||
        self.assert_length(export_dict,
 | 
					        self.assert_length(export_dict,
 | 
				
			||||||
@@ -98,7 +98,7 @@ class RealmExportTest(ZulipTestCase):
 | 
				
			|||||||
        result = self.client_get('/json/export/realm')
 | 
					        result = self.client_get('/json/export/realm')
 | 
				
			||||||
        self.assert_json_success(result)
 | 
					        self.assert_json_success(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        export_dict = result.json()['public_exports']
 | 
					        export_dict = result.json()['exports']
 | 
				
			||||||
        self.assertEqual(export_dict[0]['path'], path_id)
 | 
					        self.assertEqual(export_dict[0]['path'], path_id)
 | 
				
			||||||
        self.assertEqual(export_dict[0]['acting_user_id'], admin.id)
 | 
					        self.assertEqual(export_dict[0]['acting_user_id'], admin.id)
 | 
				
			||||||
        self.assert_length(export_dict,
 | 
					        self.assert_length(export_dict,
 | 
				
			||||||
@@ -126,5 +126,5 @@ class RealmExportTest(ZulipTestCase):
 | 
				
			|||||||
                                         event_time=timezone_now()))
 | 
					                                         event_time=timezone_now()))
 | 
				
			||||||
        RealmAuditLog.objects.bulk_create(exports)
 | 
					        RealmAuditLog.objects.bulk_create(exports)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        result = public_only_realm_export(self.client_post, admin)
 | 
					        result = export_realm(self.client_post, admin)
 | 
				
			||||||
        self.assert_json_error(result, 'Exceeded rate limit.')
 | 
					        self.assert_json_error(result, 'Exceeded rate limit.')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,10 +8,11 @@ from zerver.decorator import require_realm_admin
 | 
				
			|||||||
from zerver.models import RealmAuditLog, UserProfile
 | 
					from zerver.models import RealmAuditLog, UserProfile
 | 
				
			||||||
from zerver.lib.queue import queue_json_publish
 | 
					from zerver.lib.queue import queue_json_publish
 | 
				
			||||||
from zerver.lib.response import json_error, json_success
 | 
					from zerver.lib.response import json_error, json_success
 | 
				
			||||||
from zerver.lib.export import get_public_exports_serialized
 | 
					from zerver.lib.export import get_realm_exports_serialized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_realm_admin
 | 
					@require_realm_admin
 | 
				
			||||||
def public_only_realm_export(request: HttpRequest, user: UserProfile) -> HttpResponse:
 | 
					def export_realm(request: HttpRequest, user: UserProfile) -> HttpResponse:
 | 
				
			||||||
 | 
					    # Currently only supports public-data-only exports.
 | 
				
			||||||
    event_type = RealmAuditLog.REALM_EXPORTED
 | 
					    event_type = RealmAuditLog.REALM_EXPORTED
 | 
				
			||||||
    event_time = timezone_now()
 | 
					    event_time = timezone_now()
 | 
				
			||||||
    realm = user.realm
 | 
					    realm = user.realm
 | 
				
			||||||
@@ -32,7 +33,7 @@ def public_only_realm_export(request: HttpRequest, user: UserProfile) -> HttpRes
 | 
				
			|||||||
                                       acting_user=user)
 | 
					                                       acting_user=user)
 | 
				
			||||||
    # Using the deferred_work queue processor to avoid
 | 
					    # Using the deferred_work queue processor to avoid
 | 
				
			||||||
    # killing the process after 60s
 | 
					    # killing the process after 60s
 | 
				
			||||||
    event = {'type': event_type,
 | 
					    event = {'type': "realm_export",
 | 
				
			||||||
             'time': event_time,
 | 
					             'time': event_time,
 | 
				
			||||||
             'realm_id': realm.id,
 | 
					             'realm_id': realm.id,
 | 
				
			||||||
             'user_profile_id': user.id,
 | 
					             'user_profile_id': user.id,
 | 
				
			||||||
@@ -41,6 +42,6 @@ def public_only_realm_export(request: HttpRequest, user: UserProfile) -> HttpRes
 | 
				
			|||||||
    return json_success()
 | 
					    return json_success()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_realm_admin
 | 
					@require_realm_admin
 | 
				
			||||||
def get_public_exports(request: HttpRequest, user: UserProfile) -> HttpResponse:
 | 
					def get_realm_exports(request: HttpRequest, user: UserProfile) -> HttpResponse:
 | 
				
			||||||
    public_exports = get_public_exports_serialized(user)
 | 
					    realm_exports = get_realm_exports_serialized(user)
 | 
				
			||||||
    return json_success({"public_exports": public_exports})
 | 
					    return json_success({"exports": realm_exports})
 | 
				
			||||||
@@ -610,7 +610,7 @@ class DeferredWorker(QueueProcessingWorker):
 | 
				
			|||||||
                (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id,
 | 
					                (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id,
 | 
				
			||||||
                                                               require_active=False)
 | 
					                                                               require_active=False)
 | 
				
			||||||
                do_mark_stream_messages_as_read(user_profile, client, stream)
 | 
					                do_mark_stream_messages_as_read(user_profile, client, stream)
 | 
				
			||||||
        elif event['type'] == 'realm_exported':
 | 
					        elif event['type'] == 'realm_export':
 | 
				
			||||||
            realm = Realm.objects.get(id=event['realm_id'])
 | 
					            realm = Realm.objects.get(id=event['realm_id'])
 | 
				
			||||||
            output_dir = tempfile.mkdtemp(prefix="zulip-export-")
 | 
					            output_dir = tempfile.mkdtemp(prefix="zulip-export-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -637,6 +637,5 @@ class DeferredWorker(QueueProcessingWorker):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # For future frontend use, also notify administrator
 | 
					            # For future frontend use, also notify administrator
 | 
				
			||||||
            # clients that the export happened, including sending the
 | 
					            # clients that the export happened.
 | 
				
			||||||
            # url.
 | 
					            notify_export_completed(user_profile)
 | 
				
			||||||
            notify_export_completed(user_profile, public_url)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ import zerver.views.realm
 | 
				
			|||||||
import zerver.views.digest
 | 
					import zerver.views.digest
 | 
				
			||||||
import zerver.views.messages
 | 
					import zerver.views.messages
 | 
				
			||||||
from zerver.context_processors import latest_info_context
 | 
					from zerver.context_processors import latest_info_context
 | 
				
			||||||
import zerver.views.public_export
 | 
					import zerver.views.realm_export
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zerver.lib.rest import rest_dispatch
 | 
					from zerver.lib.rest import rest_dispatch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -398,10 +398,10 @@ v1_api_and_json_patterns = [
 | 
				
			|||||||
    url(r'^calls/create$', rest_dispatch,
 | 
					    url(r'^calls/create$', rest_dispatch,
 | 
				
			||||||
        {'GET': 'zerver.views.video_calls.get_zoom_url'}),
 | 
					        {'GET': 'zerver.views.video_calls.get_zoom_url'}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Used for public-only realm exporting
 | 
					    # Used realm data exporting
 | 
				
			||||||
    url(r'^export/realm$', rest_dispatch,
 | 
					    url(r'^export/realm$', rest_dispatch,
 | 
				
			||||||
        {'POST': 'zerver.views.public_export.public_only_realm_export',
 | 
					        {'POST': 'zerver.views.realm_export.export_realm',
 | 
				
			||||||
         'GET': 'zerver.views.public_export.get_public_exports'}),
 | 
					         'GET': 'zerver.views.realm_export.get_realm_exports'}),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# These views serve pages (HTML). As such, their internationalization
 | 
					# These views serve pages (HTML). As such, their internationalization
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user