realm-deactivation: Send email to owners as part of deactivation.

Creates a new "realm_deactivated" email that can be sent to realm
owners as part of `do_deactivate_realm`, via a boolean flag,
`email_owners`.

This flag is set to `False` when `do_deactivate_realm` is used for
realm exports or changing a realm's subdomain, so that the active
organization owners are not emailed in those cases.

This flag is optional for the `deactivate_realm` management command,
but as there is no active user passed in that case, then the email
is sent without referencing who deactivated the realm.

It is passed as `True` for the support analytics view, but the email
that is generated does not include information about the support
admin user who completed the request for organization deactivation.

When an active organization owner deactivates the organization, then
the flag is `True` and an email is sent to them as well as any other
active organization owners, with a slight variation in the email text
for those two cases.

Adds specific tests for when `email_owners` is passed as `True`. All
existing tests for other functionality of `do_deactivate_user` pass
the flag as `False`.

Adds `localize` from django.util.formats as a jinja env filter so
that the dates in these emails are internationlized for the owner's
default language setting in the "realm_deactivated" email templates.

Fixes #24685.
This commit is contained in:
Lauryn Menard 2023-09-25 22:59:44 +02:00 committed by Tim Abbott
parent 2eaf098c5d
commit 673a01ea0c
22 changed files with 290 additions and 40 deletions

View File

@ -4319,7 +4319,10 @@ class StripeTest(StripeTestCase):
self.assertEqual(last_ledger_entry.licenses_at_next_renewal, 20) self.assertEqual(last_ledger_entry.licenses_at_next_renewal, 20)
do_deactivate_realm( do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" get_realm("zulip"),
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
plan.refresh_from_db() plan.refresh_from_db()
@ -4355,7 +4358,10 @@ class StripeTest(StripeTestCase):
) )
do_deactivate_realm( do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" get_realm("zulip"),
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
self.assertTrue(get_realm("zulip").deactivated) self.assertTrue(get_realm("zulip").deactivated)
do_reactivate_realm(get_realm("zulip")) do_reactivate_realm(get_realm("zulip"))

View File

@ -1426,6 +1426,7 @@ class TestSupportEndpoint(ZulipTestCase):
lear_realm, lear_realm,
acting_user=self.example_user("iago"), acting_user=self.example_user("iago"),
deactivation_reason="owner_request", deactivation_reason="owner_request",
email_owners=True,
) )
self.assert_in_success_response(["lear deactivated"], result) self.assert_in_success_response(["lear deactivated"], result)

View File

@ -444,7 +444,10 @@ def support(
# TODO: Add support for deactivation reason in the support UI that'll be passed # TODO: Add support for deactivation reason in the support UI that'll be passed
# here. # here.
do_deactivate_realm( do_deactivate_realm(
realm, acting_user=acting_user, deactivation_reason="owner_request" realm,
acting_user=acting_user,
deactivation_reason="owner_request",
email_owners=True,
) )
context["success_message"] = f"{realm.string_id} deactivated." context["success_message"] = f"{realm.string_id} deactivated."
elif scrub_realm: elif scrub_realm:

View File

@ -0,0 +1,21 @@
{% extends "zerver/emails/email_base_default.html" %}
{% set localized_date = event_date|localize %}
{% block illustration %}
<img src="{{ email_images_base_url }}/email_logo.png" alt=""/>
{% endblock %}
{% block content %}
<p>
{% if acting_user and initiated_deactivation %}
{% trans %}You have deactivated your Zulip organization, {{ realm_name }}, on {{ localized_date }}.{% endtrans %}
{% elif acting_user %}
{% trans %}Your Zulip organization, {{ realm_name }}, was deactivated by {{ deactivating_owner }} on {{ localized_date }}.{% endtrans %}
{% else %}
{% trans %}Your Zulip organization, {{ realm_name }}, was deactivated on {{ localized_date }}.{% endtrans %}
{% endif %}
</p>
<p>
{% trans %}If you have any questions or concerns, please reply to this email as soon as possible.{% endtrans %}
</p>
{% endblock %}

View File

@ -0,0 +1 @@
{% trans %}Your Zulip organization {{ realm_name }} has been deactivated{% endtrans %}

View File

@ -0,0 +1,10 @@
{% set localized_date = event_date|localize %}
{% if acting_user and initiated_deactivation %}
{% trans %}You have deactivated your Zulip organization, {{ realm_name }}, on {{ localized_date }}.{% endtrans %}
{% elif acting_user %}
{% trans %}Your Zulip organization, {{ realm_name }}, was deactivated by {{ deactivating_owner }} on {{ localized_date }}.{% endtrans %}
{% else %}
{% trans %}Your Zulip organization, {{ realm_name }}, was deactivated on {{ localized_date }}.{% endtrans %}
{% endif %}
{% trans%}If you have any questions or concerns, please reply to this email as soon as possible.{% endtrans %}

View File

@ -116,7 +116,9 @@ with test_server_running(
do_reactivate_user(guest_user, acting_user=None) do_reactivate_user(guest_user, acting_user=None)
# Test realm deactivated error # Test realm deactivated error
do_deactivate_realm(guest_user.realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
guest_user.realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
client = Client( client = Client(
email=email, email=email,

View File

@ -98,7 +98,10 @@ def do_change_realm_subdomain(
if add_deactivated_redirect: if add_deactivated_redirect:
placeholder_realm = do_create_realm(old_subdomain, realm.name) placeholder_realm = do_create_realm(old_subdomain, realm.name)
do_deactivate_realm( do_deactivate_realm(
placeholder_realm, acting_user=None, deactivation_reason="subdomain_change" placeholder_realm,
acting_user=None,
deactivation_reason="subdomain_change",
email_owners=False,
) )
do_add_deactivated_redirect(placeholder_realm, realm.url) do_add_deactivated_redirect(placeholder_realm, realm.url)

View File

@ -2,8 +2,10 @@ import logging
from email.headerregistry import Address from email.headerregistry import Address
from typing import Any, Dict, Literal, Optional, Tuple, Union from typing import Any, Dict, Literal, Optional, Tuple, Union
import zoneinfo
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.utils.timezone import get_current_timezone_name as timezone_get_current_timezone_name
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -15,9 +17,10 @@ from zerver.actions.user_settings import do_delete_avatar_image
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.message import parse_message_time_limit_setting, update_first_visible_message_id from zerver.lib.message import parse_message_time_limit_setting, update_first_visible_message_id
from zerver.lib.retention import move_messages_to_archive from zerver.lib.retention import move_messages_to_archive
from zerver.lib.send_email import FromAddress, send_email_to_admins from zerver.lib.send_email import FromAddress, send_email, send_email_to_admins
from zerver.lib.sessions import delete_realm_user_sessions from zerver.lib.sessions import delete_realm_user_sessions
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
from zerver.lib.timezone import canonicalize_timezone
from zerver.lib.upload import delete_message_attachments from zerver.lib.upload import delete_message_attachments
from zerver.lib.user_counts import realm_user_count_by_role from zerver.lib.user_counts import realm_user_count_by_role
from zerver.lib.user_groups import ( from zerver.lib.user_groups import (
@ -501,6 +504,7 @@ def do_deactivate_realm(
*, *,
acting_user: Optional[UserProfile], acting_user: Optional[UserProfile],
deactivation_reason: RealmDeactivationReasonType, deactivation_reason: RealmDeactivationReasonType,
email_owners: bool,
) -> None: ) -> None:
""" """
Deactivate this realm. Do NOT deactivate the users -- we need to be able to Deactivate this realm. Do NOT deactivate the users -- we need to be able to
@ -557,6 +561,12 @@ def do_deactivate_realm(
# declared in zerver/lib/safe_session_cached_db.py enforces this. # declared in zerver/lib/safe_session_cached_db.py enforces this.
delete_realm_user_sessions(realm) delete_realm_user_sessions(realm)
# Flag to send deactivated realm email to organization owners; is false
# for realm exports and realm subdomain changes so that those actions
# do not email active organization owners.
if email_owners:
do_send_realm_deactivation_email(realm, acting_user)
def do_reactivate_realm(realm: Realm) -> None: def do_reactivate_realm(realm: Realm) -> None:
if not realm.deactivated: if not realm.deactivated:
@ -800,3 +810,64 @@ def do_send_realm_reactivation_email(realm: Realm, *, acting_user: Optional[User
language=language, language=language,
context=context, context=context,
) )
def do_send_realm_deactivation_email(realm: Realm, acting_user: Optional[UserProfile]) -> None:
shared_context: Dict[str, Any] = {
"realm_name": realm.name,
}
deactivation_time = timezone_now()
owners = set(realm.get_human_owner_users())
anonymous_deactivation = False
# The realm was deactivated via the deactivate_realm management command.
if acting_user is None:
anonymous_deactivation = True
# This realm was deactivated from the support panel; we do not share the
# deactivating user's information in this case.
if acting_user is not None and acting_user not in owners:
anonymous_deactivation = True
for owner in owners:
owner_tz = owner.timezone
if owner_tz == "":
owner_tz = timezone_get_current_timezone_name()
local_date = deactivation_time.astimezone(
zoneinfo.ZoneInfo(canonicalize_timezone(owner_tz))
).date()
if anonymous_deactivation:
context = dict(
acting_user=False,
initiated_deactivation=False,
event_date=local_date,
**shared_context,
)
else:
assert acting_user is not None
if owner == acting_user:
context = dict(
acting_user=True,
initiated_deactivation=True,
event_date=local_date,
**shared_context,
)
else:
context = dict(
acting_user=True,
initiated_deactivation=False,
deactivating_owner=acting_user.full_name,
event_date=local_date,
**shared_context,
)
send_email(
"zerver/emails/realm_deactivated",
to_emails=[owner.delivery_email],
from_name=FromAddress.security_email_from_name(language=owner.default_language),
from_address=FromAddress.SUPPORT,
language=owner.default_language,
context=context,
realm=realm,
)

View File

@ -21,10 +21,15 @@ class Command(ZulipBaseCommand):
help="Reason for deactivation", help="Reason for deactivation",
required=True, required=True,
) )
parser.add_argument(
"--email_owners",
action="store_true",
help="Whether to email organization owners about realm deactivation",
)
self.add_realm_args(parser, required=True) self.add_realm_args(parser, required=True)
@override @override
def handle(self, *args: Any, **options: str) -> None: def handle(self, *args: Any, **options: Any) -> None:
realm = self.get_realm(options) realm = self.get_realm(options)
deactivation_reason = options["deactivation_reason"] deactivation_reason = options["deactivation_reason"]
@ -38,8 +43,12 @@ class Command(ZulipBaseCommand):
print("The realm", options["realm_id"], "is already deactivated.") print("The realm", options["realm_id"], "is already deactivated.")
return return
send_realm_deactivation_email = options["email_owners"]
print("Deactivating", options["realm_id"]) print("Deactivating", options["realm_id"])
do_deactivate_realm( do_deactivate_realm(
realm, acting_user=None, deactivation_reason=cast(Any, deactivation_reason) realm,
acting_user=None,
deactivation_reason=cast(Any, deactivation_reason),
email_owners=send_realm_deactivation_email,
) )
print("Done!") print("Done!")

View File

@ -203,7 +203,10 @@ class Command(ZulipBaseCommand):
if options["deactivate_realm"]: if options["deactivate_realm"]:
print(f"\033[94mDeactivating realm\033[0m: {realm.string_id}") print(f"\033[94mDeactivating realm\033[0m: {realm.string_id}")
do_deactivate_realm( do_deactivate_realm(
realm, acting_user=None, deactivation_reason="self_hosting_migration" realm,
acting_user=None,
deactivation_reason="self_hosting_migration",
email_owners=False,
) )
def percent_callback(bytes_transferred: Any) -> None: def percent_callback(bytes_transferred: Any) -> None:

View File

@ -420,7 +420,9 @@ class TestRealmAuditLog(ZulipTestCase):
def test_realm_activation(self) -> None: def test_realm_activation(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")
user = self.example_user("desdemona") user = self.example_user("desdemona")
do_deactivate_realm(realm, acting_user=user, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=user, deactivation_reason="owner_request", email_owners=False
)
log_entry = RealmAuditLog.objects.get( log_entry = RealmAuditLog.objects.get(
realm=realm, event_type=RealmAuditLog.REALM_DEACTIVATED, acting_user=user realm=realm, event_type=RealmAuditLog.REALM_DEACTIVATED, acting_user=user
) )

View File

@ -215,7 +215,10 @@ class AuthBackendTest(ZulipTestCase):
# Verify auth fails with a deactivated realm # Verify auth fails with a deactivated realm
do_deactivate_realm( do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request" user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
result = backend.authenticate(**good_kwargs) result = backend.authenticate(**good_kwargs)
@ -4992,7 +4995,10 @@ class FetchAPIKeyTest(ZulipTestCase):
def test_deactivated_realm(self) -> None: def test_deactivated_realm(self) -> None:
do_deactivate_realm( do_deactivate_realm(
self.user_profile.realm, acting_user=None, deactivation_reason="owner_request" self.user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
result = self.client_post( result = self.client_post(
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
@ -5056,7 +5062,10 @@ class DevFetchAPIKeyTest(ZulipTestCase):
def test_deactivated_realm(self) -> None: def test_deactivated_realm(self) -> None:
do_deactivate_realm( do_deactivate_realm(
self.user_profile.realm, acting_user=None, deactivation_reason="owner_request" self.user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email)) result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email))
self.assert_json_error_contains(result, "This organization has been deactivated", 401) self.assert_json_error_contains(result, "This organization has been deactivated", 401)
@ -6350,7 +6359,10 @@ class TestLDAP(ZulipLDAPTestCase):
backend = self.backend backend = self.backend
email = "nonexisting@zulip.com" email = "nonexisting@zulip.com"
do_deactivate_realm( do_deactivate_realm(
backend._realm, acting_user=None, deactivation_reason="owner_request" backend._realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
with self.assertRaisesRegex(Exception, "Realm has been deactivated"): with self.assertRaisesRegex(Exception, "Realm has been deactivated"):
backend.get_or_build_user(email, _LDAPUser()) backend.get_or_build_user(email, _LDAPUser())
@ -7473,7 +7485,10 @@ class JWTFetchAPIKeyTest(ZulipTestCase):
def test_inactive_realm_failure(self) -> None: def test_inactive_realm_failure(self) -> None:
payload = {"email": self.email} payload = {"email": self.email}
do_deactivate_realm( do_deactivate_realm(
self.user_profile.realm, acting_user=None, deactivation_reason="owner_request" self.user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
with self.settings(JWT_AUTH_KEYS={"zulip": {"key": "key1", "algorithms": ["HS256"]}}): with self.settings(JWT_AUTH_KEYS={"zulip": {"key": "key1", "algorithms": ["HS256"]}}):
key = settings.JWT_AUTH_KEYS["zulip"]["key"] key = settings.JWT_AUTH_KEYS["zulip"]["key"]

View File

@ -583,7 +583,10 @@ class DeactivatedRealmTest(ZulipTestCase):
""" """
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm( do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" get_realm("zulip"),
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
result = self.client_post( result = self.client_post(
@ -652,7 +655,10 @@ class DeactivatedRealmTest(ZulipTestCase):
""" """
do_deactivate_realm( do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" get_realm("zulip"),
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
user_profile = self.example_user("hamlet") user_profile = self.example_user("hamlet")
api_key = get_api_key(user_profile) api_key = get_api_key(user_profile)

View File

@ -161,7 +161,10 @@ class EmailChangeTestCase(ZulipTestCase):
activation_url = self.generate_email_change_link(new_email) activation_url = self.generate_email_change_link(new_email)
do_deactivate_realm( do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request" user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
response = self.client_get(activation_url) response = self.client_get(activation_url)

View File

@ -1283,7 +1283,10 @@ class TestMissedMessageEmailMessages(ZulipTestCase):
mm_address = create_missed_message_address(user_profile, message) mm_address = create_missed_message_address(user_profile, message)
do_deactivate_realm( do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request" user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
incoming_valid_message = EmailMessage() incoming_valid_message = EmailMessage()

View File

@ -3007,7 +3007,9 @@ class NormalActionsTest(BaseAction):
# correct because were one to somehow compute page_params (as # correct because were one to somehow compute page_params (as
# this test does), but that's not actually possible. # this test does), but that's not actually possible.
with self.verify_action(state_change_expected=False) as events: with self.verify_action(state_change_expected=False) as events:
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
check_realm_deactivated("events[0]", events[0]) check_realm_deactivated("events[0]", events[0])
def test_do_mark_onboarding_step_as_read(self) -> None: def test_do_mark_onboarding_step_as_read(self) -> None:

View File

@ -1670,7 +1670,9 @@ class AnalyticsBouncerTest(BouncerTestCase):
do_set_realm_authentication_methods(zephyr_realm, new_auth_method_dict, acting_user=user) do_set_realm_authentication_methods(zephyr_realm, new_auth_method_dict, acting_user=user)
# Deactivation is synced. # Deactivation is synced.
do_deactivate_realm(zephyr_realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
zephyr_realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
send_server_data_to_push_bouncer() send_server_data_to_push_bouncer()
check_counts(5, 5, 1, 1, 7) check_counts(5, 5, 1, 1, 7)

View File

@ -9,6 +9,7 @@ from unittest import mock, skipUnless
import orjson import orjson
from django.conf import settings from django.conf import settings
from django.core import mail
from django.test import override_settings from django.test import override_settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from typing_extensions import override from typing_extensions import override
@ -314,7 +315,9 @@ class RealmTest(ZulipTestCase):
hamlet_id = self.example_user("hamlet").id hamlet_id = self.example_user("hamlet").id
get_user_profile_by_id(hamlet_id) get_user_profile_by_id(hamlet_id)
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
user = get_user_profile_by_id(hamlet_id) user = get_user_profile_by_id(hamlet_id)
self.assertTrue(user.realm.deactivated) self.assertTrue(user.realm.deactivated)
@ -361,7 +364,9 @@ class RealmTest(ZulipTestCase):
delay=timedelta(hours=1), delay=timedelta(hours=1),
) )
self.assertEqual(ScheduledEmail.objects.count(), 1) self.assertEqual(ScheduledEmail.objects.count(), 1)
do_deactivate_realm(user.realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
user.realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertEqual(ScheduledEmail.objects.count(), 0) self.assertEqual(ScheduledEmail.objects.count(), 0)
def test_do_change_realm_description_clears_cached_descriptions(self) -> None: def test_do_change_realm_description_clears_cached_descriptions(self) -> None:
@ -385,10 +390,14 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip") realm = get_realm("zulip")
self.assertFalse(realm.deactivated) self.assertFalse(realm.deactivated)
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
def test_do_set_deactivated_redirect_on_deactivated_realm(self) -> None: def test_do_set_deactivated_redirect_on_deactivated_realm(self) -> None:
@ -396,7 +405,9 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip") realm = get_realm("zulip")
redirect_url = "new_server.zulip.com" redirect_url = "new_server.zulip.com"
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
do_add_deactivated_redirect(realm, redirect_url) do_add_deactivated_redirect(realm, redirect_url)
self.assertEqual(realm.deactivated_redirect, redirect_url) self.assertEqual(realm.deactivated_redirect, redirect_url)
@ -408,7 +419,9 @@ class RealmTest(ZulipTestCase):
def test_do_reactivate_realm(self) -> None: def test_do_reactivate_realm(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
do_reactivate_realm(realm) do_reactivate_realm(realm)
@ -438,7 +451,9 @@ class RealmTest(ZulipTestCase):
def test_realm_reactivation_link(self) -> None: def test_realm_reactivation_link(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
obj = RealmReactivationStatus.objects.create(realm=realm) obj = RealmReactivationStatus.objects.create(realm=realm)
@ -451,13 +466,17 @@ class RealmTest(ZulipTestCase):
self.assertFalse(realm.deactivated) self.assertFalse(realm.deactivated)
# Make sure the link can't be reused. # Make sure the link can't be reused.
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
response = self.client_get(confirmation_url) response = self.client_get(confirmation_url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_realm_reactivation_confirmation_object(self) -> None: def test_realm_reactivation_confirmation_object(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
obj = RealmReactivationStatus.objects.create(realm=realm) obj = RealmReactivationStatus.objects.create(realm=realm)
create_confirmation_link(obj, Confirmation.REALM_REACTIVATION) create_confirmation_link(obj, Confirmation.REALM_REACTIVATION)
@ -466,22 +485,80 @@ class RealmTest(ZulipTestCase):
self.assertEqual(confirmation.content_object, obj) self.assertEqual(confirmation.content_object, obj)
self.assertEqual(confirmation.realm, realm) self.assertEqual(confirmation.realm, realm)
def test_do_send_realm_deactivation_email_no_acting_user(self) -> None:
realm = get_realm("zulip")
do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=True
)
self.assertEqual(realm.deactivated, True)
self.assert_length(mail.outbox, 1)
self.assertIn(
"Your Zulip organization Zulip Dev has been deactivated", mail.outbox[0].subject
)
self.assertIn("Your Zulip organization, Zulip Dev, was deactivated on", mail.outbox[0].body)
def test_do_send_realm_deactivation_email_by_support(self) -> None:
realm = get_realm("lear")
king = self.lear_user("king")
king.role = UserProfile.ROLE_REALM_OWNER
king.save()
iago = self.example_user("iago")
do_deactivate_realm(
realm, acting_user=iago, deactivation_reason="owner_request", email_owners=True
)
self.assertEqual(realm.deactivated, True)
self.assert_length(mail.outbox, 1)
self.assertIn(
"Your Zulip organization Lear & Co. has been deactivated", mail.outbox[0].subject
)
self.assertIn(
"Your Zulip organization, Lear & Co., was deactivated on",
mail.outbox[0].body,
)
def test_do_send_realm_deactivation_email_by_owner(self) -> None:
realm = get_realm("zulip")
iago = self.example_user("iago")
iago.role = UserProfile.ROLE_REALM_OWNER
iago.save(update_fields=["role"])
do_deactivate_realm(
realm, acting_user=iago, deactivation_reason="owner_request", email_owners=True
)
self.assertEqual(realm.deactivated, True)
self.assert_length(mail.outbox, 2)
for email in mail.outbox:
if email.to[0] == "iago@zulip.com":
self.assertIn(
"Your Zulip organization Zulip Dev has been deactivated", email.subject
)
self.assertIn(
"You have deactivated your Zulip organization, Zulip Dev, on", email.body
)
else:
self.assertIn(
"Your Zulip organization Zulip Dev has been deactivated", email.subject
)
self.assertIn(
"Your Zulip organization, Zulip Dev, was deactivated by Iago on", email.body
)
def test_do_send_realm_reactivation_email(self) -> None: def test_do_send_realm_reactivation_email(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=None, deactivation_reason="owner_request", email_owners=False
)
self.assertEqual(realm.deactivated, True) self.assertEqual(realm.deactivated, True)
iago = self.example_user("iago") iago = self.example_user("iago")
do_send_realm_reactivation_email(realm, acting_user=iago) do_send_realm_reactivation_email(realm, acting_user=iago)
from django.core.mail import outbox
self.assert_length(outbox, 1) self.assert_length(mail.outbox, 1)
self.assertEqual(self.email_envelope_from(outbox[0]), settings.NOREPLY_EMAIL_ADDRESS) self.assertEqual(self.email_envelope_from(mail.outbox[0]), settings.NOREPLY_EMAIL_ADDRESS)
self.assertRegex( self.assertRegex(
self.email_display_from(outbox[0]), self.email_display_from(mail.outbox[0]),
rf"^testserver account security <{self.TOKENIZED_NOREPLY_REGEX}>\Z", rf"^testserver account security <{self.TOKENIZED_NOREPLY_REGEX}>\Z",
) )
self.assertIn("Reactivate your Zulip organization", outbox[0].subject) self.assertIn("Reactivate your Zulip organization", mail.outbox[0].subject)
self.assertIn("Dear former administrators", outbox[0].body) self.assertIn("Dear former administrators", mail.outbox[0].body)
admins = realm.get_human_admin_users() admins = realm.get_human_admin_users()
confirmation_url = self.get_confirmation_url_from_outbox(admins[0].delivery_email) confirmation_url = self.get_confirmation_url_from_outbox(admins[0].delivery_email)
response = self.client_get(confirmation_url) response = self.client_get(confirmation_url)

View File

@ -557,7 +557,10 @@ class PasswordResetTest(ZulipTestCase):
user_profile = self.example_user("hamlet") user_profile = self.example_user("hamlet")
email = user_profile.delivery_email email = user_profile.delivery_email
do_deactivate_realm( do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request" user_profile.realm,
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
# start the password reset process by supplying an email address # start the password reset process by supplying an email address
@ -4353,7 +4356,10 @@ class TestFindMyTeam(ZulipTestCase):
def test_find_team_deactivated_realm(self) -> None: def test_find_team_deactivated_realm(self) -> None:
do_deactivate_realm( do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" get_realm("zulip"),
acting_user=None,
deactivation_reason="owner_request",
email_owners=False,
) )
data = {"emails": self.example_email("hamlet")} data = {"emails": self.example_email("hamlet")}
result = self.client_post("/accounts/find/", data) result = self.client_post("/accounts/find/", data)

View File

@ -494,7 +494,9 @@ def update_realm(
@has_request_variables @has_request_variables
def deactivate_realm(request: HttpRequest, user: UserProfile) -> HttpResponse: def deactivate_realm(request: HttpRequest, user: UserProfile) -> HttpResponse:
realm = user.realm realm = user.realm
do_deactivate_realm(realm, acting_user=user, deactivation_reason="owner_request") do_deactivate_realm(
realm, acting_user=user, deactivation_reason="owner_request", email_owners=True
)
return json_success(request) return json_success(request)

View File

@ -5,6 +5,7 @@ from django.contrib.staticfiles.storage import staticfiles_storage
from django.template.defaultfilters import pluralize, slugify from django.template.defaultfilters import pluralize, slugify
from django.urls import reverse from django.urls import reverse
from django.utils import translation from django.utils import translation
from django.utils.formats import localize
from django.utils.timesince import timesince from django.utils.timesince import timesince
from jinja2 import Environment from jinja2 import Environment
from two_factor.plugins.phonenumber.templatetags.phonenumber import device_action from two_factor.plugins.phonenumber.templatetags.phonenumber import device_action
@ -39,6 +40,7 @@ def environment(**options: Any) -> Environment:
env.filters["display_list"] = display_list env.filters["display_list"] = display_list
env.filters["device_action"] = device_action env.filters["device_action"] = device_action
env.filters["timesince"] = timesince env.filters["timesince"] = timesince
env.filters["localize"] = localize
env.policies["json.dumps_function"] = json_dumps env.policies["json.dumps_function"] = json_dumps
env.policies["json.dumps_kwargs"] = {} env.policies["json.dumps_kwargs"] = {}