do_deactivate_realm: Add deactivation_reason kwarg.

It's going to be helpful in the future to record the reason for realm
deactivation.
- For information tracking
- For making a distinction between cases where we can allow realm owners
  to reactivate their realm via a self-serve flow (e.g.
  "owner_request") vs where we can't (ToS abuse).
This commit is contained in:
Mateusz Mandera 2024-05-19 01:30:36 +02:00 committed by Tim Abbott
parent 6af748fa77
commit 27c4e46b30
18 changed files with 103 additions and 35 deletions

View File

@ -4318,7 +4318,9 @@ class StripeTest(StripeTestCase):
self.assertEqual(last_ledger_entry.licenses, 20) self.assertEqual(last_ledger_entry.licenses, 20)
self.assertEqual(last_ledger_entry.licenses_at_next_renewal, 20) self.assertEqual(last_ledger_entry.licenses_at_next_renewal, 20)
do_deactivate_realm(get_realm("zulip"), acting_user=None) do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request"
)
plan.refresh_from_db() plan.refresh_from_db()
self.assertTrue(get_realm("zulip").deactivated) self.assertTrue(get_realm("zulip").deactivated)
@ -4352,7 +4354,9 @@ class StripeTest(StripeTestCase):
self.seat_count, True, CustomerPlan.BILLING_SCHEDULE_ANNUAL, True, False self.seat_count, True, CustomerPlan.BILLING_SCHEDULE_ANNUAL, True, False
) )
do_deactivate_realm(get_realm("zulip"), acting_user=None) do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request"
)
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

@ -1422,7 +1422,11 @@ class TestSupportEndpoint(ZulipTestCase):
result = self.client_post( result = self.client_post(
"/activity/support", {"realm_id": f"{lear_realm.id}", "status": "deactivated"} "/activity/support", {"realm_id": f"{lear_realm.id}", "status": "deactivated"}
) )
m.assert_called_once_with(lear_realm, acting_user=self.example_user("iago")) m.assert_called_once_with(
lear_realm,
acting_user=self.example_user("iago"),
deactivation_reason="owner_request",
)
self.assert_in_success_response(["lear deactivated"], result) self.assert_in_success_response(["lear deactivated"], result)
with mock.patch("corporate.views.support.do_send_realm_reactivation_email") as m: with mock.patch("corporate.views.support.do_send_realm_reactivation_email") as m:

View File

@ -447,7 +447,11 @@ def support(
f"Realm reactivation email sent to admins of {realm.string_id}." f"Realm reactivation email sent to admins of {realm.string_id}."
) )
elif status == "deactivated": elif status == "deactivated":
do_deactivate_realm(realm, acting_user=acting_user) # TODO: Add support for deactivation reason in the support UI that'll be passed
# here.
do_deactivate_realm(
realm, acting_user=acting_user, deactivation_reason="owner_request"
)
context["success_message"] = f"{realm.string_id} deactivated." context["success_message"] = f"{realm.string_id} deactivated."
elif scrub_realm: elif scrub_realm:
do_scrub_realm(realm, acting_user=acting_user) do_scrub_realm(realm, acting_user=acting_user)

View File

@ -116,7 +116,7 @@ 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) do_deactivate_realm(guest_user.realm, acting_user=None, deactivation_reason="owner_request")
client = Client( client = Client(
email=email, email=email,

View File

@ -88,7 +88,9 @@ def do_change_realm_subdomain(
# the realm has been moved to a new subdomain. # the realm has been moved to a new 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(placeholder_realm, acting_user=None) do_deactivate_realm(
placeholder_realm, acting_user=None, deactivation_reason="subdomain_change"
)
do_add_deactivated_redirect(placeholder_realm, realm.url) do_add_deactivated_redirect(placeholder_realm, realm.url)

View File

@ -455,7 +455,23 @@ def do_set_realm_user_default_setting(
send_event(realm, event, active_user_ids(realm.id)) send_event(realm, event, active_user_ids(realm.id))
def do_deactivate_realm(realm: Realm, *, acting_user: Optional[UserProfile]) -> None: RealmDeactivationReasonType = Literal[
"owner_request",
"tos_violation",
"inactivity",
"self_hosting_migration",
# When we change the subdomain of a realm, we leave
# behind a deactivated gravestone realm.
"subdomain_change",
]
def do_deactivate_realm(
realm: Realm,
*,
acting_user: Optional[UserProfile],
deactivation_reason: RealmDeactivationReasonType,
) -> 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
tell the difference between users that were intentionally deactivated, tell the difference between users that were intentionally deactivated,
@ -481,6 +497,7 @@ def do_deactivate_realm(realm: Realm, *, acting_user: Optional[UserProfile]) ->
acting_user=acting_user, acting_user=acting_user,
extra_data={ extra_data={
RealmAuditLog.ROLE_COUNT: realm_user_count_by_role(realm), RealmAuditLog.ROLE_COUNT: realm_user_count_by_role(realm),
"deactivation_reason": deactivation_reason,
}, },
) )

View File

@ -1,5 +1,5 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import Any from typing import Any, cast
from typing_extensions import override from typing_extensions import override
@ -15,11 +15,18 @@ class Command(ZulipBaseCommand):
parser.add_argument( parser.add_argument(
"--redirect_url", metavar="<redirect_url>", help="URL to which the realm has moved" "--redirect_url", metavar="<redirect_url>", help="URL to which the realm has moved"
) )
parser.add_argument(
"--deactivation_reason",
metavar="<deactivation_reason>",
help="Reason for deactivation",
required=True,
)
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: str) -> None:
realm = self.get_realm(options) realm = self.get_realm(options)
deactivation_reason = options["deactivation_reason"]
assert realm is not None # Should be ensured by parser assert realm is not None # Should be ensured by parser
@ -32,5 +39,7 @@ class Command(ZulipBaseCommand):
return return
print("Deactivating", options["realm_id"]) print("Deactivating", options["realm_id"])
do_deactivate_realm(realm, acting_user=None) do_deactivate_realm(
realm, acting_user=None, deactivation_reason=cast(Any, deactivation_reason)
)
print("Done!") print("Done!")

View File

@ -202,7 +202,9 @@ 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(realm, acting_user=None) do_deactivate_realm(
realm, acting_user=None, deactivation_reason="self_hosting_migration"
)
def percent_callback(bytes_transferred: Any) -> None: def percent_callback(bytes_transferred: Any) -> None:
print(end=".", flush=True) print(end=".", flush=True)

View File

@ -421,11 +421,15 @@ 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) do_deactivate_realm(realm, acting_user=user, deactivation_reason="owner_request")
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
) )
extra_data = log_entry.extra_data extra_data = log_entry.extra_data
deactivation_reason = extra_data["deactivation_reason"]
self.assertEqual(deactivation_reason, "owner_request")
self.check_role_count_schema(extra_data[RealmAuditLog.ROLE_COUNT]) self.check_role_count_schema(extra_data[RealmAuditLog.ROLE_COUNT])
do_reactivate_realm(realm) do_reactivate_realm(realm)

View File

@ -214,7 +214,9 @@ class AuthBackendTest(ZulipTestCase):
self.assertEqual(user_profile, result) self.assertEqual(user_profile, result)
# Verify auth fails with a deactivated realm # Verify auth fails with a deactivated realm
do_deactivate_realm(user_profile.realm, acting_user=None) do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
result = backend.authenticate(**good_kwargs) result = backend.authenticate(**good_kwargs)
self.assertIsNone(result) self.assertIsNone(result)
@ -4989,7 +4991,9 @@ class FetchAPIKeyTest(ZulipTestCase):
self.assert_json_error_contains(result, "Account is deactivated", 401) self.assert_json_error_contains(result, "Account is deactivated", 401)
def test_deactivated_realm(self) -> None: def test_deactivated_realm(self) -> None:
do_deactivate_realm(self.user_profile.realm, acting_user=None) do_deactivate_realm(
self.user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
result = self.client_post( result = self.client_post(
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username=self.email, password=initial_password(self.email)), dict(username=self.email, password=initial_password(self.email)),
@ -5051,7 +5055,9 @@ class DevFetchAPIKeyTest(ZulipTestCase):
self.assert_json_error_contains(result, "Account is deactivated", 401) self.assert_json_error_contains(result, "Account is deactivated", 401)
def test_deactivated_realm(self) -> None: def test_deactivated_realm(self) -> None:
do_deactivate_realm(self.user_profile.realm, acting_user=None) do_deactivate_realm(
self.user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
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)
@ -6343,7 +6349,9 @@ class TestLDAP(ZulipLDAPTestCase):
with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map): with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
backend = self.backend backend = self.backend
email = "nonexisting@zulip.com" email = "nonexisting@zulip.com"
do_deactivate_realm(backend._realm, acting_user=None) do_deactivate_realm(
backend._realm, acting_user=None, deactivation_reason="owner_request"
)
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())
@ -7464,7 +7472,9 @@ 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(self.user_profile.realm, acting_user=None) do_deactivate_realm(
self.user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
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"]
[algorithm] = settings.JWT_AUTH_KEYS["zulip"]["algorithms"] [algorithm] = settings.JWT_AUTH_KEYS["zulip"]["algorithms"]

View File

@ -582,7 +582,9 @@ class DeactivatedRealmTest(ZulipTestCase):
""" """
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(get_realm("zulip"), acting_user=None) do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request"
)
result = self.client_post( result = self.client_post(
"/json/messages", "/json/messages",
@ -649,7 +651,9 @@ class DeactivatedRealmTest(ZulipTestCase):
Using a webhook while in a deactivated realm fails Using a webhook while in a deactivated realm fails
""" """
do_deactivate_realm(get_realm("zulip"), acting_user=None) do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request"
)
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)
url = f"/api/v1/external/jira?api_key={api_key}&stream=jira_custom" url = f"/api/v1/external/jira?api_key={api_key}&stream=jira_custom"

View File

@ -160,7 +160,9 @@ class EmailChangeTestCase(ZulipTestCase):
self.login_user(user_profile) self.login_user(user_profile)
activation_url = self.generate_email_change_link(new_email) activation_url = self.generate_email_change_link(new_email)
do_deactivate_realm(user_profile.realm, acting_user=None) do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
response = self.client_get(activation_url) response = self.client_get(activation_url)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)

View File

@ -1282,7 +1282,9 @@ class TestMissedMessageEmailMessages(ZulipTestCase):
mm_address = create_missed_message_address(user_profile, message) mm_address = create_missed_message_address(user_profile, message)
do_deactivate_realm(user_profile.realm, acting_user=None) do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
incoming_valid_message = EmailMessage() incoming_valid_message = EmailMessage()
incoming_valid_message.set_content("TestMissedMessageEmailMessages body") incoming_valid_message.set_content("TestMissedMessageEmailMessages body")

View File

@ -2932,7 +2932,7 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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

@ -1668,7 +1668,7 @@ 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) do_deactivate_realm(zephyr_realm, acting_user=None, deactivation_reason="owner_request")
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

@ -295,7 +295,7 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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)
@ -342,7 +342,7 @@ 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) do_deactivate_realm(user.realm, acting_user=None, deactivation_reason="owner_request")
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:
@ -366,10 +366,10 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
do_deactivate_realm(realm, acting_user=None) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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:
@ -377,7 +377,7 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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)
@ -389,7 +389,7 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
do_reactivate_realm(realm) do_reactivate_realm(realm)
@ -419,7 +419,7 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
obj = RealmReactivationStatus.objects.create(realm=realm) obj = RealmReactivationStatus.objects.create(realm=realm)
@ -432,13 +432,13 @@ 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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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)
@ -449,7 +449,7 @@ class RealmTest(ZulipTestCase):
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) do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request")
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)

View File

@ -556,7 +556,9 @@ class PasswordResetTest(ZulipTestCase):
def test_password_reset_with_deactivated_realm(self) -> None: def test_password_reset_with_deactivated_realm(self) -> None:
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(user_profile.realm, acting_user=None) do_deactivate_realm(
user_profile.realm, acting_user=None, deactivation_reason="owner_request"
)
# start the password reset process by supplying an email address # start the password reset process by supplying an email address
with self.assertLogs(level="INFO") as m: with self.assertLogs(level="INFO") as m:
@ -4350,7 +4352,9 @@ class TestFindMyTeam(ZulipTestCase):
self.assertIn("Unfortunately, no Zulip Cloud accounts", message.body) self.assertIn("Unfortunately, no Zulip Cloud accounts", message.body)
def test_find_team_deactivated_realm(self) -> None: def test_find_team_deactivated_realm(self) -> None:
do_deactivate_realm(get_realm("zulip"), acting_user=None) do_deactivate_realm(
get_realm("zulip"), acting_user=None, deactivation_reason="owner_request"
)
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)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View File

@ -477,7 +477,7 @@ 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) do_deactivate_realm(realm, acting_user=user, deactivation_reason="owner_request")
return json_success(request) return json_success(request)