registration: Enable import from slack using realm registration form.

Co-authored-by: Alex Vandiver <alexmv@zulip.com>
Co-authored-by: Tim Abbott <tabbott@zulip.com>
This commit is contained in:
Aman Agrawal
2024-11-09 11:17:54 +05:30
committed by Tim Abbott
parent a11cc9a46e
commit 136c0f1c44
13 changed files with 1161 additions and 11 deletions

View File

@@ -0,0 +1,49 @@
{% extends "zerver/portico_signup.html" %}
{% set entrypoint = "register" %}
{% block title %}
<title>{{ _("Finalize organization import") }} | Zulip</title>
{% endblock %}
{% block portico_content %}
<div class="app register-page">
<div class="app-main register-page-container new-style flex full-page center">
<div class="register-form left" id="realm-import-post-process">
<div class="lead">
<h1 class="get-started">{{ _("Organization import completed!") }}</h1>
</div>
<div class="white-box">
<form method="post" class="form-inline" action="{{ url('realm_import_post_process', args=[key]) }}">
{{ csrf_input }}
<div class="input-box no-validation">
<input type='hidden' name='key' value='{{ key }}' />
</div>
<div class="input-box slack-import-extra-info">
<div class="not-editable-realm-field">
{% trans %}
No account in the imported data matched the email address you've verified with Zulip ({{ verified_email }}).
Select an account to associate your email address with.
{% endtrans %}
</div>
</div>
<div class="input-box">
<label for="email" class="inline-block label-title">{{ _("Select your account") }}</label>
<select id="realm-import-owner" name="user_id" class="required">
{% for user in users %}
<option value="{{ user.id }}">
{{ user.full_name }} ({{user.delivery_email}})
</option>
{% endfor %}
</select>
</div>
<div class="input-box">
<button type="submit" class="register-button">
{{ _("Confirm") }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "zerver/portico_signup.html" %}
{% set entrypoint = "register" %}
{% block title %}
<title>{{ _("Import from Slack") }} | Zulip</title>
{% endblock %}
{% block portico_content %}
<div class="app register-page">
<div class="app-main register-page-container new-style flex full-page center">
<div class="register-form left" id="realm-creation-form-slack-import">
<div class="lead">
<h1 class="get-started">{{ _("Import from Slack") }}</h1>
</div>
<div class="white-box">
{% if poll_for_import_completion %}
{% if import_poll_error_message %}
<p class="text-error">{{ import_poll_error_message }}</p>
{% endif %}
<input type='hidden' name='key' value='{{ key }}' id="auth_key_for_polling" />
<div class="input-box">
<label for="uploaded-file-info">{{ _("Import progress") }}</label>
<div id="slack-import-poll-status" class="not-editable-realm-field">
{{ _("Checking import status…") }}
</div>
</div>
{% else %}
<form method="post" class="form-inline" action="{{ url('import_realm_from_slack') }}">
{{ csrf_input }}
<div class="input-box no-validation">
<input type='hidden' name='key' value='{{ key }}' id="auth_key_for_file_upload"/>
</div>
<div class="input-box slack-import-extra-info">
<div class="not-editable-realm-field">
{{ _("You will immediately see a bot user OAuth token, which is a long string of numbers and characters starting with xoxb-. Copy this token. You will use it to download user and emoji data from your Slack workspace.") }}
</div>
</div>
<div class="input-box">
<label for="slack_access_token" class="inline-block label-title">{{ _('Slack bot user OAuth token') }}</label>
<input id="slack-access-token" type="text"
placeholder="xoxb-…"
maxlength="100" name="slack_access_token" required {% if slack_access_token %} value="{{ slack_access_token }}" {% endif %} />
{% if slack_access_token_validation_error %}
<p id="slack-access-token-validation-error" class="help-inline text-error">{{ slack_access_token_validation_error }}</p>
{% endif %}
</div>
<div class="input-box">
<button type="submit" class="register-button" {% if slack_access_token %} id="update-slack-access-token"{% endif %}>
{% if slack_access_token %}
{{ _("Update") }}
{% else %}
{{ _("Submit") }}
{% endif %}
</button>
</div>
</form>
{% if slack_access_token %}
<div class="input-box" id="slack-import-drag-drop-wrapper">
<label for="slack-import-drag-and-drop" class="inline-block label-title">{{ _('Import your data') }}</label>
<div id="slack-import-drag-and-drop" data-max-file-size="{{max_file_size}}"></div>
</div>
<form id="slack-import-start-upload-wrapper" method="post" class="form-inline {% if uploaded_import_file_name %}{% else %}hidden{% endif %}" action="{{ url('import_realm_from_slack') }}">
{{ csrf_input }}
<div class="input-box no-validation">
<input type='hidden' name='key' value='{{ key }}' />
<input type='hidden' name='start_slack_import' value="true" />
</div>
<div class="input-box">
<label for="uploaded-file-info">{{ _("Uploaded file") }}</label>
<div class="not-editable-realm-field" id="slack-import-uploaded-file-name">{{ uploaded_import_file_name }}</div>
</div>
<div class="input-box">
<button type="submit" class="register-button">
{{ _("Start import") }}
</button>
</div>
</form>
{% endif %}
{% endif %}
</div>
{% if poll_for_import_completion %}
{% else %}
<form method="post" class="hidden" id="cancel-slack-import-form" action="{{ url('import_realm_from_slack') }}">
{{ csrf_input }}
<input type='hidden' name='key' value="{{ key }}" />
<input type='hidden' name='cancel_import' value='true'/>
</form>
<div class="bottom-text">
{% trans %}
Or <a href="#" id="cancel-slack-import">create organization</a> without importing data.
{% endtrans %}
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -35,6 +35,7 @@ IGNORED_PHRASES = [
r"LinkedIn", r"LinkedIn",
r"LDAP", r"LDAP",
r"Markdown", r"Markdown",
r"OAuth",
r"OTP", r"OTP",
r"Pivotal", r"Pivotal",
r"Recent conversations", r"Recent conversations",

View File

@@ -569,6 +569,7 @@ html_rules: list["Rule"] = [
"description": "`placeholder` value should be translatable.", "description": "`placeholder` value should be translatable.",
"exclude_line": { "exclude_line": {
("templates/zerver/realm_creation_form.html", 'placeholder="acme"'), ("templates/zerver/realm_creation_form.html", 'placeholder="acme"'),
("templates/zerver/slack_import.html", 'placeholder="xoxb-…"'),
}, },
"exclude": { "exclude": {
"templates/corporate", "templates/corporate",

View File

@@ -1,3 +1,8 @@
import {Uppy} from "@uppy/core";
import DragDrop from "@uppy/drag-drop";
import Tus from "@uppy/tus";
import "@uppy/core/dist/style.min.css";
import "@uppy/drag-drop/dist/style.min.css";
import $ from "jquery"; import $ from "jquery";
import _ from "lodash"; import _ from "lodash";
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
@@ -382,4 +387,74 @@ $(() => {
} }
}) as EventListener); }) as EventListener);
} }
if ($("#slack-import-drag-and-drop").length > 0) {
const key = $<HTMLInputElement>("#auth_key_for_file_upload").val();
const uppy = new Uppy({
autoProceed: true,
restrictions: {
maxNumberOfFiles: 1,
minNumberOfFiles: 1,
allowedFileTypes: [".zip", "application/zip"],
},
meta: {
key,
},
});
uppy.use(DragDrop, {
target: "#slack-import-drag-and-drop",
locale: {
strings: {
// Override the default text for the drag and drop area.
dropHereOr: $t({
defaultMessage:
"Drag and drop your Slack export file here, or click to browse.",
}),
// Required by typescript to define this.
browse: $t({
defaultMessage: "Browse",
}),
},
},
});
uppy.use(Tus, {endpoint: "/api/v1/tus/", removeFingerprintOnSuccess: true});
uppy.on("upload-error", (_file, error) => {
$("#slack-import-file-upload-error").text(error.message);
});
uppy.on("upload-success", (file, _response) => {
assert(file !== undefined);
$("#slack-import-start-upload-wrapper").removeClass("hidden");
$("#slack-import-uploaded-file-name").text(file.name!);
});
}
if ($("#slack-import-poll-status").length > 0) {
const key = $<HTMLInputElement>("#auth_key_for_polling").val();
const pollInterval = 2000; // Poll every 2 seconds
let poll_id: ReturnType<typeof setTimeout> | undefined;
function checkImportStatus(): void {
$.get(`/json/realm/import/status/${key}`, {}, (response) => {
const {status, redirect} = z
.object({status: z.string(), redirect: z.string().optional()})
.parse(response);
$("#slack-import-poll-status").text(status);
if (poll_id && redirect !== undefined) {
clearInterval(poll_id);
window.location.assign(redirect);
}
});
}
// Start polling
poll_id = setInterval(checkImportStatus, pollInterval);
}
$("#cancel-slack-import").on("click", () => {
$("#cancel-slack-import-form").trigger("submit");
});
$("#slack-access-token").on("input", () => {
$("#update-slack-access-token").show();
});
}); });

View File

@@ -51,6 +51,7 @@ html {
margin-top: 0; margin-top: 0;
} }
#realm-import-post-process,
#new-realm-creation { #new-realm-creation {
.get-started { .get-started {
font-size: 2rem; font-size: 2rem;
@@ -903,6 +904,8 @@ button#register_auth_button_gitlab {
} }
} }
#realm-import-post-process,
#realm-creation-form-slack-import,
#account-deactivated-success-page-details, #account-deactivated-success-page-details,
#server-deactivate-details, #server-deactivate-details,
#remote-billing-confirm-login-form, #remote-billing-confirm-login-form,
@@ -1476,3 +1479,55 @@ button#register_auth_button_gitlab {
padding: 0; padding: 0;
} }
} }
#realm-creation-import-from-wrapper .extra-info-realm-creation-import-from {
padding-top: 5px;
}
#slack-import-drag-drop-wrapper {
width: 100%;
margin: 10px 0 0 5px;
#slack-import-drag-and-drop {
margin-right: 5px;
}
}
#realm-import-post-process,
#realm-creation-form-slack-import {
.register-button {
margin: 0 auto;
}
#update-slack-access-token {
display: none;
}
.slack-import-extra-info {
margin-top: 0;
.not-editable-realm-field {
padding-top: 0;
}
}
.input-box.no-validation {
margin: 0;
}
}
#slack-import-drag-and-drop .uppy-DragDrop-container {
border: 1px dashed hsl(0deg 0% 0%);
background-color: hsl(0deg 0% 0% / 5%);
color: hsl(0deg 0% 40%);
font-size: 1rem;
&:focus {
outline: none;
box-shadow: none;
}
&:focus-visible {
outline: 1px solid hsl(213deg 81% 79%);
}
}

View File

@@ -0,0 +1,90 @@
import logging
import shutil
import tempfile
from typing import Any
from django.conf import settings
from confirmation import settings as confirmation_settings
from zerver.actions.realm_settings import do_delete_all_realm_attachments
from zerver.actions.users import do_change_user_role
from zerver.context_processors import is_realm_import_enabled
from zerver.data_import.slack import do_convert_zipfile
from zerver.lib.import_realm import do_import_realm
from zerver.lib.upload import save_attachment_contents
from zerver.models.prereg_users import PreregistrationRealm
from zerver.models.realms import Realm
from zerver.models.users import UserProfile, get_user_by_delivery_email
logger = logging.getLogger(__name__)
def import_slack_data(event: dict[str, Any]) -> None:
# This is only possible if data imports were enqueued before the
# setting was turned off.
assert is_realm_import_enabled()
preregistration_realm = PreregistrationRealm.objects.get(id=event["preregistration_realm_id"])
string_id = preregistration_realm.string_id
output_dir = tempfile.mkdtemp(
prefix=f"import-{preregistration_realm.id}-converted-",
dir=settings.IMPORT_TMPFILE_DIRECTORY,
)
try:
with tempfile.NamedTemporaryFile(
prefix=f"import-{preregistration_realm.id}-slack-",
suffix=".zip",
dir=settings.IMPORT_TMPFILE_DIRECTORY,
) as fh:
save_attachment_contents(event["filename"], fh)
fh.flush()
do_convert_zipfile(
fh.name,
output_dir,
event["slack_access_token"],
)
realm = do_import_realm(output_dir, string_id)
realm.org_type = preregistration_realm.org_type
realm.default_language = preregistration_realm.default_language
realm.save()
# Try finding the user who imported this realm and make them owner.
try:
importing_user = get_user_by_delivery_email(preregistration_realm.email, realm)
assert (
importing_user.is_active
and not importing_user.is_bot
and not importing_user.is_mirror_dummy
)
if importing_user.role != UserProfile.ROLE_REALM_OWNER:
do_change_user_role(
importing_user, UserProfile.ROLE_REALM_OWNER, acting_user=importing_user
)
preregistration_realm.status = confirmation_settings.STATUS_USED
except UserProfile.DoesNotExist:
# If the email address that the importing user
# validated with Zulip does not appear in the data
# export, we will prompt them which account is theirs.
preregistration_realm.data_import_metadata["need_select_realm_owner"] = True
preregistration_realm.created_realm = realm
preregistration_realm.data_import_metadata["is_import_work_queued"] = False
preregistration_realm.save()
except Exception as e:
logger.exception(e)
try:
# Clean up the realm if the import failed
preregistration_realm.created_realm = None
preregistration_realm.data_import_metadata["is_import_work_queued"] = False
preregistration_realm.save()
realm = Realm.objects.get(string_id=string_id)
do_delete_all_realm_attachments(realm)
realm.delete()
except Realm.DoesNotExist:
pass
raise
finally:
shutil.rmtree(output_dir)

View File

@@ -330,6 +330,10 @@ class HomepageForm(forms.Form):
return email return email
class ImportRealmOwnerSelectionForm(forms.Form):
user_id = forms.IntegerField()
class RealmCreationForm(RealmDetailsForm): class RealmCreationForm(RealmDetailsForm):
# This form determines whether users can create a new realm. # This form determines whether users can create a new realm.
email = forms.EmailField(validators=[email_not_system_bot, email_is_not_disposable]) email = forms.EmailField(validators=[email_not_system_bot, email_is_not_disposable])

View File

@@ -2,7 +2,7 @@ import os
import shutil import shutil
from collections.abc import Iterator from collections.abc import Iterator
from io import BytesIO from io import BytesIO
from typing import Any from typing import TYPE_CHECKING, Any
from unittest import mock from unittest import mock
from unittest.mock import ANY from unittest.mock import ANY
from urllib.parse import parse_qs, urlsplit from urllib.parse import parse_qs, urlsplit
@@ -10,9 +10,14 @@ from urllib.parse import parse_qs, urlsplit
import orjson import orjson
import responses import responses
from django.conf import settings from django.conf import settings
from django.http import HttpResponse
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from requests.models import PreparedRequest from requests.models import PreparedRequest
from confirmation import settings as confirmation_settings
from confirmation.models import Confirmation, get_object_from_key
from zerver.actions.create_realm import do_create_realm
from zerver.actions.data_import import import_slack_data
from zerver.data_import.import_util import ( from zerver.data_import.import_util import (
ZerverFieldsT, ZerverFieldsT,
build_defaultstream, build_defaultstream,
@@ -48,12 +53,22 @@ from zerver.data_import.slack import (
) )
from zerver.lib.import_realm import do_import_realm from zerver.lib.import_realm import do_import_realm
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import read_test_image_file from zerver.lib.test_helpers import find_key_by_email, read_test_image_file
from zerver.lib.topic import EXPORT_TOPIC_NAME from zerver.lib.topic import EXPORT_TOPIC_NAME
from zerver.models import Message, Realm, RealmAuditLog, Recipient, UserProfile from zerver.models import (
Message,
PreregistrationRealm,
Realm,
RealmAuditLog,
Recipient,
UserProfile,
)
from zerver.models.realm_audit_logs import AuditLogEventType from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.realms import get_realm from zerver.models.realms import get_realm
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
def remove_folder(path: str) -> None: def remove_folder(path: str) -> None:
if os.path.exists(path): if os.path.exists(path):
@@ -1778,3 +1793,415 @@ class SlackImporter(ZulipTestCase):
# We need to mock EXTERNAL_HOST to be a valid domain because Slack's importer # We need to mock EXTERNAL_HOST to be a valid domain because Slack's importer
# uses it to generate email addresses for users without an email specified. # uses it to generate email addresses for users without an email specified.
do_convert_zipfile(test_slack_zip_file, output_dir, token) do_convert_zipfile(test_slack_zip_file, output_dir, token)
@mock.patch("zerver.data_import.slack.check_token_access")
@responses.activate
def test_end_to_end_slack_import(
self,
mock_check_token_access: mock.Mock,
) -> None:
# Choose import from slack
email = "ete-slack-import@zulip.com"
string_id = "ete-slack-import"
result = self.submit_realm_creation_form(
email,
realm_subdomain=string_id,
realm_name="Slack import end to end",
import_from="slack",
)
# Confirm email
self.assertEqual(result.status_code, 302)
self.assertTrue(
result["Location"].endswith(
"/accounts/new/send_confirm/?email=ete-slack-import%40zulip.com&realm_name=Slack+import+end+to+end&realm_type=10&realm_default_language=en&realm_subdomain=ete-slack-import"
)
)
result = self.client_get(result["Location"])
self.assert_in_response("check your email", result)
prereg_realm = PreregistrationRealm.objects.get(email=email)
self.assertEqual(prereg_realm.name, "Slack import end to end")
self.assertEqual(prereg_realm.data_import_metadata["import_from"], "slack")
# Redirect to slack data import form
confirmation_url = self.get_confirmation_url_from_outbox(email)
result = self.client_get(confirmation_url)
self.assert_in_success_response(["new/import/slack"], result)
confirmation_key = find_key_by_email(email)
assert confirmation_key is not None
# Check that the we show an error message if the token is invalid.
mock_check_token_access.side_effect = ValueError("Invalid slack token")
result = self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"slack_access_token": "xoxb-invalid-token",
},
)
self.assert_in_response("Invalid slack token", result)
mock_check_token_access.side_effect = None
# Mock slack API response and mark token as valid
access_token = "xoxb-valid-token"
slack_team_info_url = "https://slack.com/api/team.info"
responses.add_callback(
responses.GET,
slack_team_info_url,
callback=lambda _: (
200,
{"x-oauth-scopes": "emoji:read,users:read,users:read.email,team:read"},
orjson.dumps({"ok": True}),
),
)
result = self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"slack_access_token": access_token,
},
)
self.assertEqual(result.status_code, 200)
prereg_realm.refresh_from_db()
self.assertEqual(prereg_realm.data_import_metadata["import_from"], "slack")
self.assertEqual(prereg_realm.data_import_metadata["slack_access_token"], access_token)
# Assume user uploaded a file.
prereg_realm.data_import_metadata["uploaded_import_file_name"] = "test_slack_importer.zip"
prereg_realm.save()
# Check that deferred_work for import is queued.
with mock.patch("zerver.views.registration.queue_json_publish_rollback_unsafe") as m:
result = self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"start_slack_import": "true",
},
)
self.assert_in_success_response(["Import progress"], result)
prereg_realm.refresh_from_db()
self.assertTrue(prereg_realm.data_import_metadata["is_import_work_queued"])
m.assert_called_once_with(
"deferred_work",
{
"type": "import_slack_data",
"preregistration_realm_id": prereg_realm.id,
"filename": f"import/{prereg_realm.id}/slack.zip",
"slack_access_token": access_token,
},
)
# We don't want to test to whole realm import process here but only that
# realm import calls are made with correct arguments and different cases
# are handled well.
realm = do_create_realm(
string_id=prereg_realm.string_id,
name=prereg_realm.name,
)
with (
mock.patch(
"zerver.actions.data_import.save_attachment_contents"
) as mocked_save_attachment,
mock.patch("zerver.actions.data_import.do_convert_zipfile") as mocked_convert_zipfile,
mock.patch(
"zerver.actions.data_import.do_import_realm", return_value=realm
) as mocked_import_realm,
):
from zerver.lib.queue import queue_json_publish_rollback_unsafe
queue_json_publish_rollback_unsafe(
"deferred_work",
{
"type": "import_slack_data",
"preregistration_realm_id": prereg_realm.id,
"filename": f"import/{prereg_realm.id}/slack.zip",
"slack_access_token": access_token,
},
)
self.assertTrue(mocked_save_attachment.called)
self.assertTrue(mocked_convert_zipfile.called)
self.assertTrue(mocked_import_realm.called)
realm.refresh_from_db()
self.assertEqual(realm.org_type, prereg_realm.org_type)
self.assertEqual(realm.default_language, prereg_realm.default_language)
prereg_realm.refresh_from_db()
self.assertTrue(prereg_realm.data_import_metadata["need_select_realm_owner"])
# Confirmation key at this point is marked, used but since we
# are mocking the process, we need to do it manually here.
get_object_from_key(confirmation_key, [Confirmation.REALM_CREATION], mark_object_used=True)
result = self.client_get(f"/json/realm/import/status/{confirmation_key}")
self.assert_in_success_response(["No users matching provided email"], result)
# Create a user who become the realm owner, ideally this will be created
# as part of the import process or we will add form for user to do so.
imported_user_to_be_owner = UserProfile.objects.create(realm=realm, delivery_email=email)
imported_user_to_be_owner.set_unusable_password()
imported_user_to_be_owner.save()
def post_process_request(key: str | None = confirmation_key) -> "TestHttpResponse":
return self.client_post(
f"/realm/import/post_process/{key}",
{
"user_id": str(imported_user_to_be_owner.id),
},
)
# Show error on using wrong confirmation key.
with mock.patch(
"zerver.views.registration.render_confirmation_key_error",
return_value=HttpResponse(status=200),
) as m:
post_process_request("malformed_key")
m.assert_called_once()
# Check if we cannot find the realm, preregistration_realm is revoked.
prereg_realm.string_id = "non_existent_realm"
prereg_realm.save()
result = post_process_request()
prereg_realm.refresh_from_db()
self.assertEqual(prereg_realm.status, confirmation_settings.STATUS_REVOKED)
# Reset status for further tests.
prereg_realm.string_id = string_id
prereg_realm.status = 0
prereg_realm.save()
# Redirect user to password reset page on successful import.
result = post_process_request()
self.assertEqual(result.status_code, 302)
self.assertTrue(
result["Location"].startswith(
"http://ete-slack-import.testserver/accounts/password/reset/"
)
)
# If user refreshes the page, redirect to login page if the import was successful.
result = self.client_get(f"/realm/import/post_process/{confirmation_key}")
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "http://ete-slack-import.testserver/accounts/login/")
# Check if we render a form for user to select a user if there
# are no users matching the provided email.
prereg_realm.data_import_metadata["need_select_realm_owner"] = True
prereg_realm.save()
result = self.client_get(f"/realm/import/post_process/{confirmation_key}")
self.assert_in_success_response(["Select your account"], result)
# Check that user is redirected to this form using email confirmation link.
result = self.client_get(confirmation_url)
self.assert_in_success_response(["new/import/slack"], result)
result = self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"slack_access_token": access_token,
},
)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], f"/realm/import/post_process/{confirmation_key}")
result = self.client_get(f"/realm/import/post_process/{confirmation_key}")
self.assert_in_success_response(["Select your account"], result)
@mock.patch("zerver.actions.data_import.do_import_realm")
@mock.patch("zerver.actions.data_import.do_convert_zipfile")
@mock.patch("zerver.actions.data_import.save_attachment_contents")
def test_import_slack_data_found_user_matching_email_of_importer(
self,
mock_save_attachment_contents: mock.Mock,
mock_do_convert_zipfile: mock.Mock,
mock_do_import_realm: mock.Mock,
) -> None:
prereg_realm = PreregistrationRealm.objects.create(
string_id="test-realm-slack-import",
name="Test Realm",
email="test_import_slack_data_user@example.com",
data_import_metadata={"import_from": "slack"},
)
mock_realm = do_create_realm(
string_id=prereg_realm.string_id,
name=prereg_realm.name,
)
mock_do_import_realm.return_value = mock_realm
importing_user = UserProfile.objects.create(
realm=mock_realm,
delivery_email=prereg_realm.email,
)
event = {
"preregistration_realm_id": prereg_realm.id,
"filename": "import/test/slack.zip",
"slack_access_token": "xoxb-valid-token",
}
import_slack_data(event)
mock_save_attachment_contents.assert_called_once()
mock_do_convert_zipfile.assert_called_once_with(
mock.ANY, mock.ANY, event["slack_access_token"]
)
mock_do_import_realm.assert_called_once_with(mock.ANY, prereg_realm.string_id)
prereg_realm.refresh_from_db()
self.assertEqual(prereg_realm.status, 1) # STATUS_USED
self.assertEqual(prereg_realm.created_realm, mock_realm)
self.assertFalse(prereg_realm.data_import_metadata["is_import_work_queued"])
self.assertFalse(prereg_realm.data_import_metadata.get("need_select_realm_owner"))
# Check that the importing user was made the realm owner
importing_user.refresh_from_db()
self.assertEqual(importing_user.role, UserProfile.ROLE_REALM_OWNER)
@mock.patch("zerver.actions.data_import.do_import_realm")
@mock.patch("zerver.actions.data_import.do_convert_zipfile")
@mock.patch("zerver.actions.data_import.save_attachment_contents")
def test_import_slack_data_failure_cleanup_realm_not_created(
self,
mock_save_attachment_contents: mock.Mock,
mock_do_convert_zipfile: mock.Mock,
mock_do_import_realm: mock.Mock,
) -> None:
prereg_realm = PreregistrationRealm.objects.create(
string_id="test-realm",
name="Test Realm",
email="test@example.com",
data_import_metadata={"import_from": "slack"},
)
mock_do_import_realm.side_effect = AssertionError("Import failed")
event = {
"preregistration_realm_id": prereg_realm.id,
"filename": "import/test/slack.zip",
"slack_access_token": "xoxb-valid-token",
}
with (
self.assertRaises(AssertionError),
self.assertLogs("zerver.actions.data_import", "ERROR"),
):
import_slack_data(event)
prereg_realm.refresh_from_db()
self.assertIsNone(prereg_realm.created_realm)
self.assertFalse(prereg_realm.data_import_metadata["is_import_work_queued"])
self.assertFalse(Realm.objects.filter(string_id="test-realm").exists())
@mock.patch("zerver.actions.data_import.do_import_realm")
@mock.patch("zerver.actions.data_import.do_convert_zipfile")
@mock.patch("zerver.actions.data_import.save_attachment_contents")
def test_import_slack_data_failure_cleanup_realm_created(
self,
mock_save_attachment_contents: mock.Mock,
mock_do_convert_zipfile: mock.Mock,
mock_do_import_realm: mock.Mock,
) -> None:
prereg_realm = PreregistrationRealm.objects.create(
string_id="test-realm",
name="Test Realm",
email="test@example.com",
data_import_metadata={"import_from": "slack"},
)
do_create_realm(
string_id=prereg_realm.string_id,
name=prereg_realm.name,
)
self.assertTrue(Realm.objects.filter(string_id=prereg_realm.string_id).exists())
mock_do_import_realm.side_effect = AssertionError("Import failed")
event = {
"preregistration_realm_id": prereg_realm.id,
"filename": "import/test/slack.zip",
"slack_access_token": "xoxb-valid-token",
}
with (
self.assertRaises(AssertionError),
self.assertLogs("zerver.actions.data_import", "ERROR"),
):
import_slack_data(event)
prereg_realm.refresh_from_db()
self.assertIsNone(prereg_realm.created_realm)
self.assertFalse(prereg_realm.data_import_metadata["is_import_work_queued"])
self.assertFalse(Realm.objects.filter(string_id=prereg_realm.string_id).exists())
@responses.activate
def test_cancel_realm_import(self) -> None:
# Choose import from slack
email = "ete-slack-import@zulip.com"
self.submit_realm_creation_form(
email,
realm_subdomain="ete-slack-import",
realm_name="Slack import end to end",
import_from="slack",
)
prereg_realm = PreregistrationRealm.objects.get(email=email)
self.assertEqual(prereg_realm.data_import_metadata["import_from"], "slack")
# If the import is already in process, don't allow import cancellation.
prereg_realm.data_import_metadata["is_import_work_queued"] = True
prereg_realm.save()
confirmation_key = find_key_by_email(email)
assert confirmation_key is not None
response = self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"cancel_import": "true",
},
)
self.assert_in_success_response(["Unable to cancel import"], response)
prereg_realm.refresh_from_db()
self.assertTrue(prereg_realm.data_import_metadata["is_import_work_queued"])
# Allow cancellation if the import work is not queued.
prereg_realm.data_import_metadata["is_import_work_queued"] = False
prereg_realm.save()
response = self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"cancel_import": "true",
},
)
prereg_realm.refresh_from_db()
self.assertIsNone(prereg_realm.data_import_metadata.get("import_from"))
@responses.activate
def test_cancel_realm_import_realm_created(self) -> None:
# If user cancelled import after realm was created,
# clean up the created realm.
email = "ete-slack-import@zulip.com"
self.submit_realm_creation_form(
email,
realm_subdomain="ete-slack-import",
realm_name="Slack import end to end",
import_from="slack",
)
prereg_realm = PreregistrationRealm.objects.get(email=email)
self.assertEqual(prereg_realm.data_import_metadata["import_from"], "slack")
realm = do_create_realm(
string_id=prereg_realm.string_id,
name=prereg_realm.name,
)
self.assertTrue(Realm.objects.filter(string_id=prereg_realm.string_id).exists())
prereg_realm.created_realm = realm
prereg_realm.save()
confirmation_key = find_key_by_email(email)
assert confirmation_key is not None
# We don't allow cancellation if the complete import work is done.
with self.assertRaises(AssertionError), self.assertLogs("django.request", "ERROR"):
self.client_post(
"/new/import/slack/",
{
"key": confirmation_key,
"cancel_import": "true",
},
)

View File

@@ -7,6 +7,7 @@ from urllib.parse import urlencode, urljoin
import orjson import orjson
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, get_backends from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, get_backends
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.base import SessionBase
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -18,9 +19,11 @@ from django.shortcuts import redirect, render
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
from django.utils.translation import get_language from django.utils.translation import get_language
from django.utils.translation import gettext as _
from django_auth_ldap.backend import LDAPBackend, _LDAPUser from django_auth_ldap.backend import LDAPBackend, _LDAPUser
from pydantic import Json, NonNegativeInt, StringConstraints from pydantic import Json, NonNegativeInt, StringConstraints
from confirmation import settings as confirmation_settings
from confirmation.models import ( from confirmation.models import (
Confirmation, Confirmation,
ConfirmationKeyError, ConfirmationKeyError,
@@ -36,12 +39,14 @@ from zerver.actions.default_streams import lookup_default_stream_groups
from zerver.actions.user_settings import ( from zerver.actions.user_settings import (
do_change_full_name, do_change_full_name,
do_change_password, do_change_password,
do_change_user_delivery_email,
do_change_user_setting, do_change_user_setting,
) )
from zerver.actions.users import do_change_user_role from zerver.actions.users import do_change_user_role, generate_password_reset_url
from zerver.context_processors import ( from zerver.context_processors import (
get_realm_create_form_context, get_realm_create_form_context,
get_realm_from_request, get_realm_from_request,
is_realm_import_enabled,
login_context, login_context,
) )
from zerver.decorator import add_google_analytics, do_login, require_post from zerver.decorator import add_google_analytics, do_login, require_post
@@ -49,12 +54,13 @@ from zerver.forms import (
CaptchaRealmCreationForm, CaptchaRealmCreationForm,
FindMyTeamForm, FindMyTeamForm,
HomepageForm, HomepageForm,
ImportRealmOwnerSelectionForm,
RealmCreationForm, RealmCreationForm,
RealmRedirectForm, RealmRedirectForm,
RegistrationForm, RegistrationForm,
) )
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
from zerver.lib.exceptions import RateLimitedError from zerver.lib.exceptions import JsonableError, RateLimitedError
from zerver.lib.i18n import ( from zerver.lib.i18n import (
get_browser_language_code, get_browser_language_code,
get_default_language_for_anonymous_user, get_default_language_for_anonymous_user,
@@ -62,7 +68,9 @@ from zerver.lib.i18n import (
get_language_name, get_language_name,
) )
from zerver.lib.pysa import mark_sanitized from zerver.lib.pysa import mark_sanitized
from zerver.lib.queue import queue_json_publish_rollback_unsafe
from zerver.lib.rate_limiter import rate_limit_request_by_ip from zerver.lib.rate_limiter import rate_limit_request_by_ip
from zerver.lib.response import json_success
from zerver.lib.send_email import EmailNotDeliveredError, FromAddress, send_email from zerver.lib.send_email import EmailNotDeliveredError, FromAddress, send_email
from zerver.lib.sessions import get_expirable_session_var from zerver.lib.sessions import get_expirable_session_var
from zerver.lib.subdomains import get_subdomain from zerver.lib.subdomains import get_subdomain
@@ -77,10 +85,12 @@ from zerver.lib.typed_endpoint_validators import (
non_negative_int_or_none_validator, non_negative_int_or_none_validator,
timezone_or_empty_validator, timezone_or_empty_validator,
) )
from zerver.lib.upload import all_message_attachments
from zerver.lib.url_encoding import append_url_query_string from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.users import get_accounts_for_email from zerver.lib.users import get_accounts_for_email
from zerver.lib.zephyr import compute_mit_user_fullname from zerver.lib.zephyr import compute_mit_user_fullname
from zerver.models import ( from zerver.models import (
Message,
MultiuseInvite, MultiuseInvite,
NamedUserGroup, NamedUserGroup,
PreregistrationRealm, PreregistrationRealm,
@@ -91,7 +101,7 @@ from zerver.models import (
UserProfile, UserProfile,
) )
from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH
from zerver.models.realm_audit_logs import RealmAuditLog from zerver.models.realm_audit_logs import AuditLogEventType, RealmAuditLog
from zerver.models.realms import ( from zerver.models.realms import (
DisposableEmailError, DisposableEmailError,
DomainNotAllowedForRealmError, DomainNotAllowedForRealmError,
@@ -101,12 +111,17 @@ from zerver.models.realms import (
name_changes_disabled, name_changes_disabled,
) )
from zerver.models.streams import get_default_stream_groups from zerver.models.streams import get_default_stream_groups
from zerver.models.users import get_source_profile, get_user_by_delivery_email from zerver.models.users import (
get_source_profile,
get_user_by_delivery_email,
get_user_profile_by_id_in_realm,
)
from zerver.views.auth import ( from zerver.views.auth import (
create_preregistration_realm, create_preregistration_realm,
create_preregistration_user, create_preregistration_user,
finish_desktop_flow, finish_desktop_flow,
finish_mobile_flow, finish_mobile_flow,
get_safe_redirect_to,
redirect_and_log_into_subdomain, redirect_and_log_into_subdomain,
redirect_to_deactivation_notice, redirect_to_deactivation_notice,
) )
@@ -149,7 +164,11 @@ def get_prereg_key_and_redirect(
registration_url = reverse("accounts_register") registration_url = reverse("accounts_register")
if realm_creation: if realm_creation:
registration_url = reverse("realm_register") assert isinstance(prereg_object, PreregistrationRealm)
if prereg_object.data_import_metadata.get("import_from") == "slack":
registration_url = reverse("import_realm_from_slack")
else:
registration_url = reverse("realm_register")
return render( return render(
request, request,
@@ -186,9 +205,16 @@ def check_prereg_key(
if realm_creation: if realm_creation:
assert isinstance(prereg_object, PreregistrationRealm) assert isinstance(prereg_object, PreregistrationRealm)
# Defensive assert to make sure no mix-up in how .status is set leading to reuse if prereg_object.data_import_metadata.get("need_select_realm_owner"):
# of a PreregistrationRealm object. # Allow user to get back to the import page to select realm owner.
assert prereg_object.created_realm is None # This is for a special case where the realm import has finished
# but user closed the import process browser window and needs to
# get back to the import page from the email confirmation link.
assert prereg_object.created_realm is not None
else:
# Defensive assert to make sure no mix-up in how .status is set leading to reuse
# of a PreregistrationRealm object.
assert prereg_object.created_realm is None
else: else:
assert isinstance(prereg_object, PreregistrationUser) assert isinstance(prereg_object, PreregistrationUser)
# Defensive assert to make sure no mix-up in how .status is set leading to reuse # Defensive assert to make sure no mix-up in how .status is set leading to reuse
@@ -227,6 +253,11 @@ def accounts_register(*args: Any, **kwargs: Any) -> HttpResponse:
return registration_helper(*args, **kwargs) return registration_helper(*args, **kwargs)
@require_post
def import_realm_from_slack(*args: Any, **kwargs: Any) -> HttpResponse:
return registration_helper(*args, **kwargs)
@typed_endpoint @typed_endpoint
def registration_helper( def registration_helper(
request: HttpRequest, request: HttpRequest,
@@ -237,6 +268,9 @@ def registration_helper(
form_full_name: Annotated[str | None, ApiParamConfig("full_name")] = None, form_full_name: Annotated[str | None, ApiParamConfig("full_name")] = None,
source_realm_id: Annotated[NonNegativeInt | None, non_negative_int_or_none_validator()] = None, source_realm_id: Annotated[NonNegativeInt | None, non_negative_int_or_none_validator()] = None,
form_is_demo_organization: Annotated[str | None, ApiParamConfig("is_demo_organization")] = None, form_is_demo_organization: Annotated[str | None, ApiParamConfig("is_demo_organization")] = None,
slack_access_token: str | None = None,
start_slack_import: Json[bool] = False,
cancel_import: Json[bool] = False,
) -> HttpResponse: ) -> HttpResponse:
try: try:
prereg_object, realm_creation = check_prereg_key(request, key) prereg_object, realm_creation = check_prereg_key(request, key)
@@ -249,6 +283,117 @@ def registration_helper(
if realm_creation: if realm_creation:
assert isinstance(prereg_object, PreregistrationRealm) assert isinstance(prereg_object, PreregistrationRealm)
prereg_realm = prereg_object prereg_realm = prereg_object
if cancel_import:
if prereg_realm.created_realm or prereg_realm.data_import_metadata.get(
"is_import_work_queued"
):
# This cancellation flow is just to go back to normal
# realm creation before one has started it. If the
# user somehow triggers this operation (no longer
# visible in the UI) after that point, we just
# redirect user import status page with a message that
# import work has already started.
return TemplateResponse(
request,
"zerver/slack_import.html",
{
"import_poll_error_message": _(
"Unable to cancel import once it has started."
),
"poll_for_import_completion": True,
"key": key,
},
)
# If the user cancels the import process, it is critical
# to remove any metadata that was added during the import
# process.
# NOTE: Don't revoke the confirmation key here, to allow
# the user to continue with the registration process if
# no import flow has started. This is important otherwise
# the user will be unable to register using the same subdomain.
prereg_realm.data_import_metadata = {}
prereg_realm.save(update_fields=["data_import_metadata"])
# Return back to normal registration flow.
return HttpResponseRedirect(
reverse("get_prereg_key_and_redirect", kwargs={"confirmation_key": key})
)
if start_slack_import:
assert is_realm_import_enabled()
assert prereg_realm.data_import_metadata.get("slack_access_token") is not None
assert prereg_realm.data_import_metadata.get("uploaded_import_file_name") is not None
assert prereg_realm.data_import_metadata.get("is_import_work_queued") is not True
assert prereg_realm.created_realm is None
queue_json_publish_rollback_unsafe(
"deferred_work",
{
"type": "import_slack_data",
"preregistration_realm_id": prereg_realm.id,
"filename": f"import/{prereg_realm.id}/slack.zip",
"slack_access_token": prereg_realm.data_import_metadata["slack_access_token"],
},
)
# Avoid starting the import process multiple times.
prereg_realm.data_import_metadata["is_import_work_queued"] = True
prereg_realm.save(update_fields=["data_import_metadata"])
return TemplateResponse(
request,
"zerver/slack_import.html",
{
"poll_for_import_completion": True,
"key": key,
},
)
elif prereg_realm.data_import_metadata.get("import_from") == "slack":
if prereg_realm.data_import_metadata.get("need_select_realm_owner"):
return HttpResponseRedirect(
reverse("realm_import_post_process", kwargs={"confirmation_key": key})
)
assert is_realm_import_enabled()
context: dict[str, Any] = {
"key": key,
"max_file_size": settings.MAX_WEB_DATA_IMPORT_SIZE_MB,
}
saved_slack_access_token = prereg_realm.data_import_metadata.get("slack_access_token")
if saved_slack_access_token or slack_access_token:
if slack_access_token and slack_access_token != saved_slack_access_token:
# Verify slack token access.
from zerver.data_import.slack import (
SLACK_IMPORT_TOKEN_SCOPES,
check_token_access,
)
try:
check_token_access(slack_access_token, SLACK_IMPORT_TOKEN_SCOPES)
except Exception as e:
context["slack_access_token_validation_error"] = str(e)
return TemplateResponse(
request,
"zerver/slack_import.html",
context,
)
saved_slack_access_token = slack_access_token
prereg_realm.data_import_metadata["slack_access_token"] = slack_access_token
prereg_realm.save(update_fields=["data_import_metadata"])
context["slack_access_token"] = saved_slack_access_token
context["uploaded_import_file_name"] = prereg_realm.data_import_metadata.get(
"uploaded_import_file_name"
)
return TemplateResponse(
request,
"zerver/slack_import.html",
context,
)
password_required = True password_required = True
role = UserProfile.ROLE_REALM_OWNER role = UserProfile.ROLE_REALM_OWNER
else: else:
@@ -891,6 +1036,193 @@ def redirect_to_email_login_url(email: str) -> HttpResponseRedirect:
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
@typed_endpoint
def realm_import_status(
request: HttpRequest,
*,
confirmation_key: str,
) -> HttpResponse: # nocoverage
try:
preregistration_realm = get_object_from_key(
confirmation_key,
[Confirmation.REALM_CREATION],
mark_object_used=False,
allow_used=True,
)
except ConfirmationKeyError:
raise JsonableError(_("Unauthenticated"))
assert isinstance(preregistration_realm, PreregistrationRealm)
try:
realm = Realm.objects.get(string_id=preregistration_realm.string_id)
except Realm.DoesNotExist:
# TODO: Either store the path to the temporary conversion directory on
# preregistration_realm.data_import_metadata, or have the conversion
# process support writing updates to this for a better progress indicator.
return json_success(request, {"status": _("Converting Slack data…")})
if realm.deactivated:
# These "if" cases are in the inverse order than they're done
# in the import process, so we get the latest step that it's
# on.
if Message.objects.filter(realm_id=realm.id).exists():
return json_success(request, {"status": _("Importing messages…")})
try:
next(all_message_attachments(prefix=f"{realm.id}/"))
return json_success(request, {"status": _("Importing attachment data…")})
except StopIteration:
pass
return json_success(request, {"status": _("Importing converted Slack data…")})
if (
not preregistration_realm.data_import_metadata["need_select_realm_owner"]
and preregistration_realm.created_realm is None
):
return json_success(request, {"status": _("Finalizing import…")})
# We have a non-deactivated realm and it's linked to the prereg key
result = {"status": _("Done!")}
if not preregistration_realm.data_import_metadata["need_select_realm_owner"]:
importing_user = get_user_by_delivery_email(preregistration_realm.email, realm)
# Sanity check that this is a normal user account that can login.
assert (
importing_user.is_active
and not importing_user.is_bot
and not importing_user.is_mirror_dummy
)
# Allow setting an initial password for this first account,
# being careful to ensure this data import confirmation link
# can't be abused to reset the importing user's password in
# the future.
if (
not importing_user.has_usable_password()
and not RealmAuditLog.objects.filter(
modified_user=importing_user, event_type=AuditLogEventType.USER_PASSWORD_CHANGED
).exists()
):
result["redirect"] = generate_password_reset_url(
importing_user, default_token_generator
)
else:
result["redirect"] = get_safe_redirect_to(reverse("login"), realm.url)
else:
# The email address in the import may not match the email
# address they provided. Ask user which user they want to become.
result["status"] = _("No users matching provided email.")
result["redirect"] = reverse(
"realm_import_post_process", kwargs={"confirmation_key": confirmation_key}
)
return json_success(request, result)
@transaction.atomic(durable=True)
def realm_import_post_process(
request: HttpRequest,
confirmation_key: str,
) -> HttpResponse:
try:
preregistration_realm = get_object_from_key(
confirmation_key,
[Confirmation.REALM_CREATION],
mark_object_used=False,
allow_used=True,
)
except ConfirmationKeyError as exception:
return render_confirmation_key_error(request, exception)
assert isinstance(preregistration_realm, PreregistrationRealm)
try:
realm = Realm.objects.get(string_id=preregistration_realm.string_id)
except Realm.DoesNotExist:
# If we cannot find the realm, likely means there was
# something wrong with the import process, or it has been
# since deleted. Revoke the confirmation key to force user to
# restart the process.
preregistration_realm.status = confirmation_settings.STATUS_REVOKED
preregistration_realm.save(update_fields=["status"])
return render_confirmation_key_error(
request, ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
)
if not preregistration_realm.data_import_metadata["need_select_realm_owner"]:
return HttpResponseRedirect(get_safe_redirect_to(reverse("login"), realm.url))
if request.method == "POST":
form = ImportRealmOwnerSelectionForm(request.POST)
if form.is_valid():
# This is a highly sensitive code path, since what we're
# about to do is take control of another user account AND
# promote that account to organization owner.
#
# We need this code path for Slack import if the importing
# user's account doesn't exist in the Slack import, but we
# must be VERY careful to make sure we're in that situation.
# Validate that this PreregistrationRealm object is in the
# expected state, with no user matching the email address,
# and and having created this realm.
assert preregistration_realm.data_import_metadata["need_select_realm_owner"]
assert preregistration_realm.created_realm_id == realm.id
assert preregistration_realm.status != confirmation_settings.STATUS_USED
# ID of the imported user account that the importing user has
# selected to become their account.
user_id = form.cleaned_data["user_id"]
# Validate that a normal user account that can login was selected.
importing_user = get_user_profile_by_id_in_realm(user_id, realm)
assert (
importing_user.is_active
and not importing_user.is_bot
and not importing_user.is_mirror_dummy
)
# Promote to realm owner and set email address to what
# we've validated. This is safe because we've previously
# validated that this specific confirmation link was used
# to create this specific realm and cannot have been used
# to finish creating an account yet.
do_change_user_role(
importing_user, UserProfile.ROLE_REALM_OWNER, acting_user=importing_user
)
do_change_user_delivery_email(
importing_user, preregistration_realm.email, acting_user=importing_user
)
preregistration_realm.status = confirmation_settings.STATUS_USED
preregistration_realm.data_import_metadata["need_select_realm_owner"] = False
preregistration_realm.save()
# End by letting the importing user set a password for their new account.
assert (
not importing_user.has_usable_password()
and not RealmAuditLog.objects.filter(
modified_user=importing_user, event_type=AuditLogEventType.USER_PASSWORD_CHANGED
).exists()
)
return HttpResponseRedirect(
generate_password_reset_url(importing_user, default_token_generator)
)
claimable_users = UserProfile.objects.filter(
realm=realm, is_active=True, is_bot=False, is_mirror_dummy=False
)
context = {
"users": claimable_users,
"verified_email": preregistration_realm.email,
"key": confirmation_key,
}
return TemplateResponse(
request,
"zerver/realm_import_post_process.html",
context,
)
@add_google_analytics @add_google_analytics
def create_realm(request: HttpRequest, creation_key: str | None = None) -> HttpResponse: def create_realm(request: HttpRequest, creation_key: str | None = None) -> HttpResponse:
try: try:

View File

@@ -12,6 +12,7 @@ from django.utils.translation import gettext as _
from django.utils.translation import override as override_language from django.utils.translation import override as override_language
from typing_extensions import override from typing_extensions import override
from zerver.actions.data_import import import_slack_data
from zerver.actions.message_flags import do_mark_stream_messages_as_read from zerver.actions.message_flags import do_mark_stream_messages_as_read
from zerver.actions.message_send import internal_send_private_message from zerver.actions.message_send import internal_send_private_message
from zerver.actions.realm_export import notify_realm_export from zerver.actions.realm_export import notify_realm_export
@@ -236,6 +237,8 @@ class DeferredWorker(QueueProcessingWorker):
) )
for realm in realms_to_scrub: for realm in realms_to_scrub:
scrub_deactivated_realm(realm) scrub_deactivated_realm(realm)
elif event["type"] == "import_slack_data":
import_slack_data(event)
end = time.time() end = time.time()
logger.info( logger.info(

View File

@@ -639,6 +639,9 @@ NAGIOS_BOT_HOST = SYSTEM_BOT_REALM + "." + EXTERNAL_HOST
# Use half of the available CPUs for data import purposes. # Use half of the available CPUs for data import purposes.
DEFAULT_DATA_EXPORT_IMPORT_PARALLELISM = (len(os.sched_getaffinity(0)) // 2) or 1 DEFAULT_DATA_EXPORT_IMPORT_PARALLELISM = (len(os.sched_getaffinity(0)) // 2) or 1
# Use the default tmpfile path for automated imports
IMPORT_TMPFILE_DIRECTORY: str | None = None
# How long after the last upgrade to nag users that the server needs # How long after the last upgrade to nag users that the server needs
# to be upgraded because of likely security releases in the meantime. # to be upgraded because of likely security releases in the meantime.
# Default is 18 months, constructed as 12 months before someone should # Default is 18 months, constructed as 12 months before someone should

View File

@@ -146,7 +146,10 @@ from zerver.views.registration import (
create_realm, create_realm,
find_account, find_account,
get_prereg_key_and_redirect, get_prereg_key_and_redirect,
import_realm_from_slack,
new_realm_send_confirm, new_realm_send_confirm,
realm_import_post_process,
realm_import_status,
realm_redirect, realm_redirect,
realm_register, realm_register,
signup_send_confirm, signup_send_confirm,
@@ -622,6 +625,16 @@ i18n_urls = [
), ),
path("accounts/register/", accounts_register, name="accounts_register"), path("accounts/register/", accounts_register, name="accounts_register"),
path("realm/register/", realm_register, name="realm_register"), path("realm/register/", realm_register, name="realm_register"),
path(
"realm/import/post_process/<confirmation_key>",
realm_import_post_process,
name="realm_import_post_process",
),
path("new/import/slack/", import_realm_from_slack, name="import_realm_from_slack"),
path(
"json/realm/import/status/<confirmation_key>",
realm_import_status,
),
path( path(
"accounts/do_confirm/<confirmation_key>", "accounts/do_confirm/<confirmation_key>",
get_prereg_key_and_redirect, get_prereg_key_and_redirect,