mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
auth: Add an organization reactivation flow with admin confirmation.
This adds a web flow and management command for reactivating a Zulip organization, with confirmation from one of the organization administrators. Further work is needed to make the emails nicer (ideally, we'd send one email with all the admins on the `To` line, but the `send_email` library doesn't support that). Fixes #10783. With significant tweaks to the email text by tabbott.
This commit is contained in:
committed by
Tim Abbott
parent
10e8e2acac
commit
d00b889402
@@ -70,8 +70,11 @@ def create_confirmation_link(obj: ContentType, host: str,
|
||||
confirmation_type: int,
|
||||
url_args: Optional[Dict[str, str]]=None) -> str:
|
||||
key = generate_key()
|
||||
realm = None
|
||||
if hasattr(obj, 'realm'):
|
||||
realm = obj.realm
|
||||
Confirmation.objects.create(content_object=obj, date_sent=timezone_now(), confirmation_key=key,
|
||||
realm=obj.realm, type=confirmation_type)
|
||||
realm=realm, type=confirmation_type)
|
||||
return confirmation_url(key, host, confirmation_type, url_args)
|
||||
|
||||
def confirmation_url(confirmation_key: str, host: str,
|
||||
@@ -99,6 +102,7 @@ class Confirmation(models.Model):
|
||||
SERVER_REGISTRATION = 5
|
||||
MULTIUSE_INVITE = 6
|
||||
REALM_CREATION = 7
|
||||
REALM_REACTIVATION = 8
|
||||
type = models.PositiveSmallIntegerField() # type: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -121,6 +125,7 @@ _properties = {
|
||||
'zerver.views.registration.accounts_home_from_multiuse_invite',
|
||||
validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS),
|
||||
Confirmation.REALM_CREATION: ConfirmationType('check_prereg_key_and_redirect'),
|
||||
Confirmation.REALM_REACTIVATION: ConfirmationType('zerver.views.realm.realm_reactivation'),
|
||||
}
|
||||
|
||||
def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str:
|
||||
|
||||
38
templates/zerver/emails/realm_reactivation.source.html
Normal file
38
templates/zerver/emails/realm_reactivation.source.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "zerver/emails/compiled/email_base_default.html" %}
|
||||
|
||||
{% block illustration %}
|
||||
<img src="{{ email_images_base_uri }}/registration_confirmation.png"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
{% trans %}
|
||||
Dear former administrators of {{ realm_name }},
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans %}
|
||||
One of your administrators requested reactivation of the
|
||||
previously deactivated Zulip organization hosted at {{ realm_uri }}. If you'd
|
||||
like to do confirm that request and reactivate the organization, please click here:
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<a class="button" href="{{ confirmation_url }}">{{ _('Reactivate organization') }}</a>
|
||||
<p>
|
||||
{% trans %}
|
||||
If the request was in error, you can take no action and this link
|
||||
will expire in 24 hours.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans %}
|
||||
Feel free to give us a shout at
|
||||
<a href="mailto:{{ support_email }}">{{ support_email }}</a>,
|
||||
if you have any questions.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
{{ _("Cheers,") }}<br />
|
||||
{{ _("Team Zulip") }}
|
||||
</p>
|
||||
{% endblock %}
|
||||
1
templates/zerver/emails/realm_reactivation.subject
Normal file
1
templates/zerver/emails/realm_reactivation.subject
Normal file
@@ -0,0 +1 @@
|
||||
Reactivate your Zulip organization
|
||||
21
templates/zerver/emails/realm_reactivation.txt
Normal file
21
templates/zerver/emails/realm_reactivation.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
{% trans %}Dear former administrators of {{ realm_name }},{% endtrans %}
|
||||
|
||||
{% trans %}
|
||||
One of your administrators requested reactivation of the previously
|
||||
deactivated Zulip organization hosted at
|
||||
{{ realm_uri }}.
|
||||
If you'd like to do confirm that request and reactivate the
|
||||
organization, please click here:
|
||||
{% endtrans %}
|
||||
{{ _('To reactivate organization, please click here:') }}
|
||||
<{{ confirmation_url }}>
|
||||
|
||||
{% trans %}
|
||||
If the request was in error, you can take no action and this link
|
||||
will expire in 24 hours.
|
||||
{% endtrans %}
|
||||
|
||||
{% trans %}Feel free to give us a shout at {{ support_email }},if you have any questions.{% endtrans %}
|
||||
|
||||
{{ _("Cheers,") }}<br />
|
||||
{{ _("Team Zulip") }}
|
||||
14
templates/zerver/realm_reactivation.html
Normal file
14
templates/zerver/realm_reactivation.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "zerver/portico_signup.html" %}
|
||||
|
||||
{% block portico_content %}
|
||||
<div class="app portico-page">
|
||||
<div class="app-main portico-page-container center-block flex full-page account-creation new-style">
|
||||
<div class="inline-block">
|
||||
|
||||
<div class="get-started">
|
||||
<h1>{{ _("Your organization has been successfully reactivated.") }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
templates/zerver/realm_reactivation_link_error.html
Normal file
14
templates/zerver/realm_reactivation_link_error.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "zerver/portico_signup.html" %}
|
||||
|
||||
{% block portico_content %}
|
||||
<div class="app portico-page">
|
||||
<div class="app-main portico-page-container center-block flex full-page account-creation new-style">
|
||||
<div class="inline-block">
|
||||
|
||||
<div class="get-started">
|
||||
<h1>{{ _("The organization reactivation link has expired or is not valid.") }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -11,4 +11,4 @@ LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2018/11/07/zulip-1-9-relea
|
||||
# Typically, adding a dependency only requires a minor version bump, and
|
||||
# removing a dependency requires a major version bump.
|
||||
|
||||
PROVISION_VERSION = '26.14'
|
||||
PROVISION_VERSION = '26.15'
|
||||
|
||||
@@ -5140,3 +5140,15 @@ def missing_any_realm_internal_bots() -> bool:
|
||||
.annotate(Count('id')))
|
||||
realm_count = Realm.objects.count()
|
||||
return any(bot_counts.get(email, 0) < realm_count for email in bot_emails)
|
||||
|
||||
def do_send_realm_reactivation_email(realm: Realm) -> None:
|
||||
confirmation_url = create_confirmation_link(realm, realm.host, Confirmation.REALM_REACTIVATION)
|
||||
admins = realm.get_admin_users()
|
||||
context = {'confirmation_url': confirmation_url,
|
||||
'realm_uri': realm.uri,
|
||||
'realm_name': realm.name}
|
||||
for admin in admins:
|
||||
send_email(
|
||||
'zerver/emails/realm_reactivation', to_email=admin.email,
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
from_name="Zulip Account Security", context=context)
|
||||
|
||||
23
zerver/management/commands/send_realm_reactivation_email.py
Normal file
23
zerver/management/commands/send_realm_reactivation_email.py
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand, CommandError
|
||||
from zerver.lib.send_email import send_email, FromAddress
|
||||
from zerver.lib.actions import do_send_realm_reactivation_email
|
||||
|
||||
from typing import Any
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Sends realm reactivation email to admins"""
|
||||
|
||||
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||
self.add_realm_args(parser, True)
|
||||
|
||||
def handle(self, *args: Any, **options: str) -> None:
|
||||
realm = self.get_realm(options)
|
||||
assert realm is not None
|
||||
if not realm.deactivated:
|
||||
raise CommandError("The realm %s is already active." % (realm.name,))
|
||||
print('Sending email to admins')
|
||||
do_send_realm_reactivation_email(realm)
|
||||
print('Done!')
|
||||
@@ -11,7 +11,7 @@ from typing import List, Dict, Any, Optional
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
from zerver.lib.actions import do_create_user
|
||||
from zerver.lib.actions import do_create_user, do_deactivate_realm, do_reactivate_realm
|
||||
from zerver.lib.management import ZulipBaseCommand, CommandError, check_config
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import stdout_suppressed
|
||||
@@ -287,3 +287,11 @@ class TestPasswordRestEmail(ZulipTestCase):
|
||||
tokenized_no_reply_email = parseaddr(from_email)[1]
|
||||
self.assertTrue(re.search(self.TOKENIZED_NOREPLY_REGEX, tokenized_no_reply_email))
|
||||
self.assertIn("Psst. Word on the street is that you", outbox[0].body)
|
||||
|
||||
class TestRealmReactivationEmail(ZulipTestCase):
|
||||
COMMAND_NAME = "send_realm_reactivation_email"
|
||||
|
||||
def test_if_realm_not_deactivated(self) -> None:
|
||||
realm = get_realm('zulip')
|
||||
with self.assertRaisesRegex(CommandError, "The realm %s is already active." % (realm.name,)):
|
||||
call_command(self.COMMAND_NAME, "--realm=zulip")
|
||||
|
||||
@@ -3,6 +3,7 @@ import datetime
|
||||
import ujson
|
||||
import re
|
||||
import mock
|
||||
from email.utils import parseaddr
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
@@ -20,8 +21,10 @@ from zerver.lib.actions import (
|
||||
do_scrub_realm,
|
||||
create_stream_if_needed,
|
||||
do_change_plan_type,
|
||||
do_send_realm_reactivation_email
|
||||
)
|
||||
|
||||
from confirmation.models import create_confirmation_link, Confirmation
|
||||
from zerver.lib.send_email import send_future_email
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import tornado_redirected_to_list
|
||||
@@ -195,6 +198,39 @@ class RealmTest(ZulipTestCase):
|
||||
do_deactivate_realm(realm)
|
||||
self.assertTrue(realm.deactivated)
|
||||
|
||||
def test_realm_reactivation_link(self) -> None:
|
||||
realm = get_realm('zulip')
|
||||
do_deactivate_realm(realm)
|
||||
self.assertTrue(realm.deactivated)
|
||||
confirmation_url = create_confirmation_link(realm, realm.host, Confirmation.REALM_REACTIVATION)
|
||||
response = self.client_get(confirmation_url)
|
||||
self.assert_in_success_response(['Your organization has been successfully reactivated'], response)
|
||||
realm = get_realm('zulip')
|
||||
self.assertFalse(realm.deactivated)
|
||||
|
||||
def test_do_send_realm_reactivation_email(self) -> None:
|
||||
realm = get_realm('zulip')
|
||||
do_send_realm_reactivation_email(realm)
|
||||
from django.core.mail import outbox
|
||||
self.assertEqual(len(outbox), 1)
|
||||
from_email = outbox[0].from_email
|
||||
tokenized_no_reply_email = parseaddr(from_email)[1]
|
||||
self.assertIn("Zulip Account Security", from_email)
|
||||
self.assertTrue(re.search(self.TOKENIZED_NOREPLY_REGEX, tokenized_no_reply_email))
|
||||
self.assertIn('Reactivate your Zulip organization', outbox[0].subject)
|
||||
self.assertIn('To reactivate organization, please click here:', outbox[0].body)
|
||||
admins = realm.get_admin_users()
|
||||
confirmation_url = self.get_confirmation_url_from_outbox(admins[0].email)
|
||||
response = self.client_get(confirmation_url)
|
||||
self.assert_in_success_response(['Your organization has been successfully reactivated'], response)
|
||||
realm = get_realm('zulip')
|
||||
self.assertFalse(realm.deactivated)
|
||||
|
||||
def test_realm_reactivation_with_random_link(self) -> None:
|
||||
random_link = "/reactivate/5e89081eb13984e0f3b130bf7a4121d153f1614b"
|
||||
response = self.client_get(random_link)
|
||||
self.assert_in_success_response(['The organization reactivation link has expired or is not valid.'], response)
|
||||
|
||||
def test_change_notifications_stream(self) -> None:
|
||||
# We need an admin user.
|
||||
email = 'iago@zulip.com'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
from typing import Any, Dict, Optional, List
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -15,6 +16,7 @@ from zerver.lib.actions import (
|
||||
do_set_realm_signup_notifications_stream,
|
||||
do_set_realm_property,
|
||||
do_deactivate_realm,
|
||||
do_reactivate_realm,
|
||||
)
|
||||
from zerver.lib.i18n import get_available_language_codes
|
||||
from zerver.lib.request import has_request_variables, REQ, JsonableError
|
||||
@@ -24,6 +26,7 @@ from zerver.lib.streams import access_stream_by_id
|
||||
from zerver.lib.domains import validate_domain
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zerver.forms import check_subdomain_available as check_subdomain
|
||||
from confirmation.models import get_object_from_key, Confirmation, ConfirmationKeyException
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@@ -169,3 +172,12 @@ def check_subdomain_available(request: HttpRequest, subdomain: str) -> HttpRespo
|
||||
return json_success({"msg": "available"})
|
||||
except ValidationError as e:
|
||||
return json_success({"msg": e.message})
|
||||
|
||||
def realm_reactivation(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
||||
try:
|
||||
realm = get_object_from_key(confirmation_key, Confirmation.REALM_REACTIVATION)
|
||||
except ConfirmationKeyException:
|
||||
return render(request, 'zerver/realm_reactivation_link_error.html')
|
||||
do_reactivate_realm(realm)
|
||||
context = {"realm": realm}
|
||||
return render(request, 'zerver/realm_reactivation.html', context)
|
||||
|
||||
@@ -458,6 +458,10 @@ i18n_urls = [
|
||||
url(r'^new/(?P<creation_key>[\w]+)$',
|
||||
zerver.views.registration.create_realm, name='zerver.views.create_realm'),
|
||||
|
||||
# Realm Reactivation
|
||||
url(r'^reactivate/(?P<confirmation_key>[\w]+)', zerver.views.realm.realm_reactivation,
|
||||
name='zerver.views.realm.realm_reactivation'),
|
||||
|
||||
# Global public streams (Zulip's way of doing archives)
|
||||
url(r'^archive/streams/(?P<stream_id>\d+)/topics/(?P<topic_name>[^/]+)$',
|
||||
zerver.views.archive.archive,
|
||||
|
||||
Reference in New Issue
Block a user