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,
PreregistrationUser,
Realm,
RealmReactivationStatus,
UserProfile,
get_org_type_display_name,
get_realm,
@ -313,8 +314,9 @@ def support(
]
confirmations += get_confirmations([Confirmation.MULTIUSE_INVITE], multiuse_invite_ids)
realm_reactivation_status_objects = RealmReactivationStatus.objects.filter(realm__in=realms)
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

View File

@ -21,6 +21,7 @@ from zerver.models import (
Attachment,
Realm,
RealmAuditLog,
RealmReactivationStatus,
RealmUserDefault,
ScheduledEmail,
Stream,
@ -460,7 +461,9 @@ def do_change_realm_plan_type(
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(
realm=realm,
acting_user=acting_user,

View File

@ -145,6 +145,7 @@ ALL_ZULIP_TABLES = {
"zerver_realmemoji",
"zerver_realmfilter",
"zerver_realmplayground",
"zerver_realmreactivationstatus",
"zerver_realmuserdefault",
"zerver_recipient",
"zerver_scheduledemail",
@ -184,6 +185,7 @@ NON_EXPORTED_TABLES = {
"zerver_multiuseinvite_streams",
"zerver_preregistrationuser",
"zerver_preregistrationuser_streams",
"zerver_realmreactivationstatus",
# Missed message addresses are low value to export since
# missed-message email addresses include the server's hostname and
# 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)
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):
APNS = 1
GCM = 2

View File

@ -31,6 +31,7 @@ from zerver.models import (
Message,
Realm,
RealmAuditLog,
RealmReactivationStatus,
RealmUserDefault,
ScheduledEmail,
Stream,
@ -354,7 +355,9 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None)
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)
self.assert_in_success_response(
["Your organization has been successfully reactivated"], response
@ -362,6 +365,11 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip")
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:
realm = get_realm("zulip")
do_deactivate_realm(realm, acting_user=None)

View File

@ -38,7 +38,7 @@ from zerver.lib.validator import (
check_string_or_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
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:
try:
realm = get_object_from_key(
confirmation_key, [Confirmation.REALM_REACTIVATION], mark_object_used=False
obj = get_object_from_key(
confirmation_key, [Confirmation.REALM_REACTIVATION], mark_object_used=True
)
except ConfirmationKeyException:
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)
# TODO: After reactivating the realm, the confirmation link needs to be revoked in some way.
context = {"realm": realm}
return render(request, "zerver/realm_reactivation.html", context)