signup: Implement use of MultiUseInvite.status attribute.

This allows us to revoke MultiUseInvites by changing their .status
instead of deleting them (which has been deleting the helpful tracking
information on PreregistrationUsers about which MultiUseInvite they came
from).
This commit is contained in:
Mateusz Mandera 2022-09-12 00:39:43 +02:00 committed by Tim Abbott
parent 977a043d03
commit d201229df8
6 changed files with 58 additions and 5 deletions

View File

@ -99,7 +99,7 @@ def get_object_from_key(
raise ConfirmationKeyException(ConfirmationKeyException.EXPIRED)
if mark_object_used:
# MultiuseInvite objects have no status field, since they are
# MultiuseInvite objects do not use the STATUS_USED status, since they are
# intended to be used more than once.
assert confirmation.type != Confirmation.MULTIUSE_INVITE
assert hasattr(obj, "status")

View File

@ -10,6 +10,7 @@ from django.utils.translation import gettext as _
from analytics.lib.counts import COUNT_STATS, do_increment_logging_stat
from analytics.models import RealmCount
from confirmation import settings as confirmation_settings
from confirmation.models import Confirmation, confirmation_url, create_confirmation_link
from zerver.lib.email_validation import (
get_existing_user_errors,
@ -392,7 +393,8 @@ def do_revoke_multi_use_invite(multiuse_invite: MultiuseInvite) -> None:
Confirmation.objects.filter(
content_type=content_type, object_id=multiuse_invite.id
).delete()
multiuse_invite.delete()
multiuse_invite.status = confirmation_settings.STATUS_REVOKED
multiuse_invite.save(update_fields=["status"])
notify_invites_changed(realm)

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-09-11 22:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0421_migrate_pronouns_custom_profile_fields"),
]
operations = [
migrations.AddField(
model_name="multiuseinvite",
name="status",
field=models.IntegerField(default=0),
),
]

View File

@ -2333,6 +2333,12 @@ class MultiuseInvite(models.Model):
realm = models.ForeignKey(Realm, on_delete=CASCADE)
invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"])
# status for tracking whether the invite has been revoked.
# If revoked, set to confirmation.settings.STATUS_REVOKED.
# STATUS_USED is not supported, because these objects are supposed
# to be usable multiple times.
status = models.IntegerField(default=0)
class EmailChangeStatus(models.Model):
new_email = models.EmailField()

View File

@ -40,6 +40,7 @@ from zerver.actions.invites import (
do_create_multiuse_invite_link,
do_get_invites_controlled_by_user,
do_invite_users,
do_revoke_multi_use_invite,
)
from zerver.actions.realm_settings import (
do_deactivate_realm,
@ -2710,10 +2711,13 @@ class InvitationsTestCase(InviteUserBase):
)
result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
self.assertEqual(result.status_code, 200)
self.assertIsNone(MultiuseInvite.objects.filter(id=multiuse_invite.id).first())
self.assertEqual(
MultiuseInvite.objects.get(id=multiuse_invite.id).status,
confirmation_settings.STATUS_REVOKED,
)
# Test that trying to double-delete fails
error_result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
self.assert_json_error(error_result, "No such invitation")
self.assert_json_error(error_result, "Invitation has already been revoked")
# Test deleting owner mutiuse_invite.
multiuse_invite = MultiuseInvite.objects.create(
@ -2731,7 +2735,10 @@ class InvitationsTestCase(InviteUserBase):
self.login("desdemona")
result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
self.assert_json_success(result)
self.assertIsNone(MultiuseInvite.objects.filter(id=multiuse_invite.id).first())
self.assertEqual(
MultiuseInvite.objects.get(id=multiuse_invite.id).status,
confirmation_settings.STATUS_REVOKED,
)
# Test deleting multiuse invite from another realm
mit_realm = get_realm("zephyr")
@ -2749,6 +2756,10 @@ class InvitationsTestCase(InviteUserBase):
)
self.assert_json_error(error_result, "No such invitation")
non_existent_id = MultiuseInvite.objects.count() + 9999
error_result = self.client_delete(f"/json/invites/multiuse/{non_existent_id}")
self.assert_json_error(error_result, "No such invitation")
def test_successful_resend_invitation(self) -> None:
"""
A POST call to /json/invites/<ID>/resend should send an invitation reminder email
@ -3056,6 +3067,18 @@ class MultiuseInviteTest(ZulipTestCase):
self.assertEqual(result.status_code, 404)
self.assert_in_response("The confirmation link has expired or been deactivated.", result)
def test_revoked_multiuse_link(self) -> None:
email = self.nonreg_email("newuser")
invite_link = self.generate_multiuse_invite_link()
multiuse_invite = MultiuseInvite.objects.last()
assert multiuse_invite is not None
do_revoke_multi_use_invite(multiuse_invite)
result = self.client_post(invite_link, {"email": email})
self.assertEqual(result.status_code, 404)
self.assert_in_response("We couldn't find your confirmation link in the system.", result)
def test_invalid_multiuse_link(self) -> None:
email = self.nonreg_email("newuser")
invite_link = "/join/invalid_key/"

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from confirmation import settings as confirmation_settings
from zerver.actions.invites import (
do_create_multiuse_invite_link,
do_get_invites_controlled_by_user,
@ -147,6 +148,9 @@ def revoke_multiuse_invite(
check_if_owner_required(invite.invited_as, user_profile)
if invite.status == confirmation_settings.STATUS_REVOKED:
raise JsonableError(_("Invitation has already been revoked"))
do_revoke_multi_use_invite(invite)
return json_success(request)