realm_reactivation: Prevent realm reactivation link reuse.

This uses the approach analogical to EmailChangeStatus for email change
confirmation links.
This commit is contained in:
Mateusz Mandera 2022-07-26 15:48:26 +02:00 committed by Tim Abbott
parent 46c6f33b10
commit cf74d7d140
7 changed files with 66 additions and 8 deletions

View File

@ -36,6 +36,7 @@ from zerver.models import (
MultiuseInvite, MultiuseInvite,
PreregistrationUser, PreregistrationUser,
Realm, Realm,
RealmReactivationStatus,
UserProfile, UserProfile,
get_org_type_display_name, get_org_type_display_name,
get_realm, get_realm,
@ -313,8 +314,9 @@ def support(
] ]
confirmations += get_confirmations([Confirmation.MULTIUSE_INVITE], multiuse_invite_ids) confirmations += get_confirmations([Confirmation.MULTIUSE_INVITE], multiuse_invite_ids)
realm_reactivation_status_objects = RealmReactivationStatus.objects.filter(realm__in=realms)
confirmations += get_confirmations( confirmations += get_confirmations(
[Confirmation.REALM_REACTIVATION], [realm.id for realm in realms] [Confirmation.REALM_REACTIVATION], [obj.id for obj in realm_reactivation_status_objects]
) )
context["confirmations"] = confirmations context["confirmations"] = confirmations

View File

@ -21,6 +21,7 @@ from zerver.models import (
Attachment, Attachment,
Realm, Realm,
RealmAuditLog, RealmAuditLog,
RealmReactivationStatus,
RealmUserDefault, RealmUserDefault,
ScheduledEmail, ScheduledEmail,
Stream, Stream,
@ -460,7 +461,9 @@ def do_change_realm_plan_type(
def do_send_realm_reactivation_email(realm: Realm, *, acting_user: Optional[UserProfile]) -> None: def do_send_realm_reactivation_email(realm: Realm, *, acting_user: Optional[UserProfile]) -> None:
url = create_confirmation_link(realm, Confirmation.REALM_REACTIVATION) obj = RealmReactivationStatus.objects.create(realm=realm)
url = create_confirmation_link(obj, Confirmation.REALM_REACTIVATION)
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
realm=realm, realm=realm,
acting_user=acting_user, acting_user=acting_user,

View File

@ -145,6 +145,7 @@ ALL_ZULIP_TABLES = {
"zerver_realmemoji", "zerver_realmemoji",
"zerver_realmfilter", "zerver_realmfilter",
"zerver_realmplayground", "zerver_realmplayground",
"zerver_realmreactivationstatus",
"zerver_realmuserdefault", "zerver_realmuserdefault",
"zerver_recipient", "zerver_recipient",
"zerver_scheduledemail", "zerver_scheduledemail",
@ -184,6 +185,7 @@ NON_EXPORTED_TABLES = {
"zerver_multiuseinvite_streams", "zerver_multiuseinvite_streams",
"zerver_preregistrationuser", "zerver_preregistrationuser",
"zerver_preregistrationuser_streams", "zerver_preregistrationuser_streams",
"zerver_realmreactivationstatus",
# Missed message addresses are low value to export since # Missed message addresses are low value to export since
# missed-message email addresses include the server's hostname and # missed-message email addresses include the server's hostname and
# expire after a few days. # expire after a few days.

View File

@ -0,0 +1,32 @@
# Generated by Django 4.0.6 on 2022-07-25 20:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0399_preregistrationuser_multiuse_invite"),
]
operations = [
migrations.CreateModel(
name="RealmReactivationStatus",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("status", models.IntegerField(default=0)),
(
"realm",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="zerver.realm"
),
),
],
),
]

View File

@ -2315,6 +2315,15 @@ class EmailChangeStatus(models.Model):
realm: Realm = models.ForeignKey(Realm, on_delete=CASCADE) realm: Realm = models.ForeignKey(Realm, on_delete=CASCADE)
class RealmReactivationStatus(models.Model):
id: int = models.AutoField(auto_created=True, primary_key=True, verbose_name="ID")
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status: int = models.IntegerField(default=0)
realm: Realm = models.ForeignKey(Realm, on_delete=CASCADE)
class AbstractPushDeviceToken(models.Model): class AbstractPushDeviceToken(models.Model):
APNS = 1 APNS = 1
GCM = 2 GCM = 2

View File

@ -31,6 +31,7 @@ from zerver.models import (
Message, Message,
Realm, Realm,
RealmAuditLog, RealmAuditLog,
RealmReactivationStatus,
RealmUserDefault, RealmUserDefault,
ScheduledEmail, ScheduledEmail,
Stream, Stream,
@ -354,7 +355,9 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip") realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None) do_deactivate_realm(realm, acting_user=None)
self.assertTrue(realm.deactivated) self.assertTrue(realm.deactivated)
confirmation_url = create_confirmation_link(realm, Confirmation.REALM_REACTIVATION)
obj = RealmReactivationStatus.objects.create(realm=realm)
confirmation_url = create_confirmation_link(obj, Confirmation.REALM_REACTIVATION)
response = self.client_get(confirmation_url) response = self.client_get(confirmation_url)
self.assert_in_success_response( self.assert_in_success_response(
["Your organization has been successfully reactivated"], response ["Your organization has been successfully reactivated"], response
@ -362,6 +365,11 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip") realm = get_realm("zulip")
self.assertFalse(realm.deactivated) self.assertFalse(realm.deactivated)
# Make sure the link can't be reused.
do_deactivate_realm(realm, acting_user=None)
response = self.client_get(confirmation_url)
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)

View File

@ -38,7 +38,7 @@ from zerver.lib.validator import (
check_string_or_int, check_string_or_int,
to_non_negative_int, to_non_negative_int,
) )
from zerver.models import Realm, RealmUserDefault, UserProfile from zerver.models import Realm, RealmReactivationStatus, RealmUserDefault, UserProfile
from zerver.views.user_settings import check_settings_values from zerver.views.user_settings import check_settings_values
ORG_TYPE_IDS: List[int] = [t["id"] for t in Realm.ORG_TYPES.values()] ORG_TYPE_IDS: List[int] = [t["id"] for t in Realm.ORG_TYPES.values()]
@ -327,14 +327,16 @@ def check_subdomain_available(request: HttpRequest, subdomain: str) -> HttpRespo
def realm_reactivation(request: HttpRequest, confirmation_key: str) -> HttpResponse: def realm_reactivation(request: HttpRequest, confirmation_key: str) -> HttpResponse:
try: try:
realm = get_object_from_key( obj = get_object_from_key(
confirmation_key, [Confirmation.REALM_REACTIVATION], mark_object_used=False confirmation_key, [Confirmation.REALM_REACTIVATION], mark_object_used=True
) )
except ConfirmationKeyException: except ConfirmationKeyException:
return render(request, "zerver/realm_reactivation_link_error.html", status=404) return render(request, "zerver/realm_reactivation_link_error.html", status=404)
assert isinstance(realm, Realm)
assert isinstance(obj, RealmReactivationStatus)
realm = obj.realm
do_reactivate_realm(realm) do_reactivate_realm(realm)
# TODO: After reactivating the realm, the confirmation link needs to be revoked in some way.
context = {"realm": realm} context = {"realm": realm}
return render(request, "zerver/realm_reactivation.html", context) return render(request, "zerver/realm_reactivation.html", context)