snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
=============================
Django Confirmation Changelog
=============================

28
confirmation/LICENSE.txt Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of the author nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

10
confirmation/README.txt Normal file
View File

@@ -0,0 +1,10 @@
===================
Django Confirmation
===================
This is a generic object confirmation system for Django applications.
For installation instructions, see the file "INSTALL.txt" in this
directory; for instructions on how to use this application, and on
what it provides, see the file "overview.txt" in the "docs/"
directory.

22
confirmation/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
VERSION = (0, 9, "pre")

View File

View File

@@ -0,0 +1,39 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Confirmation",
fields=[
(
"id",
models.AutoField(
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
),
),
("object_id", models.PositiveIntegerField()),
("date_sent", models.DateTimeField(verbose_name="sent")),
(
"confirmation_key",
models.CharField(max_length=40, verbose_name="activation key"),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.ContentType"
),
),
],
options={
"verbose_name": "confirmation email",
"verbose_name_plural": "confirmation emails",
},
bases=(models.Model,),
),
]

View File

@@ -0,0 +1,90 @@
# Generated by Django 5.0.7 on 2024-08-13 19:41
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("confirmation", "0001_initial"),
("confirmation", "0002_realmcreationkey"),
("confirmation", "0003_emailchangeconfirmation"),
("confirmation", "0004_remove_confirmationmanager"),
("confirmation", "0005_confirmation_realm"),
("confirmation", "0006_realmcreationkey_presume_email_valid"),
("confirmation", "0007_add_indexes"),
("confirmation", "0008_confirmation_expiry_date"),
("confirmation", "0009_confirmation_expiry_date_backfill"),
("confirmation", "0010_alter_confirmation_expiry_date"),
("confirmation", "0011_alter_confirmation_expiry_date"),
("confirmation", "0012_alter_confirmation_id"),
("confirmation", "0013_alter_realmcreationkey_id"),
("confirmation", "0014_confirmation_confirmatio_content_80155a_idx"),
]
initial = True
dependencies = [
("contenttypes", "0001_initial"),
("zerver", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="RealmCreationKey",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"creation_key",
models.CharField(db_index=True, max_length=40, verbose_name="activation key"),
),
(
"date_created",
models.DateTimeField(default=django.utils.timezone.now, verbose_name="created"),
),
("presume_email_valid", models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name="Confirmation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("object_id", models.PositiveIntegerField(db_index=True)),
("date_sent", models.DateTimeField(db_index=True)),
("confirmation_key", models.CharField(db_index=True, max_length=40)),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
),
),
("type", models.PositiveSmallIntegerField()),
(
"realm",
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.realm"
),
),
("expiry_date", models.DateTimeField(db_index=True, null=True)),
],
options={
"unique_together": {("type", "confirmation_key")},
"indexes": [
models.Index(
fields=["content_type", "object_id"], name="confirmatio_content_80155a_idx"
)
],
},
),
]

View File

@@ -0,0 +1,27 @@
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="RealmCreationKey",
fields=[
(
"id",
models.AutoField(
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
),
),
("creation_key", models.CharField(max_length=40, verbose_name="activation key")),
(
"date_created",
models.DateTimeField(default=django.utils.timezone.now, verbose_name="created"),
),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 1.10.4 on 2017-01-17 09:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0002_realmcreationkey"),
]
operations = [
migrations.CreateModel(
name="EmailChangeConfirmation",
fields=[],
options={
"proxy": True,
},
bases=("confirmation.confirmation",),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 1.11.2 on 2017-07-08 04:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0003_emailchangeconfirmation"),
]
operations = [
migrations.DeleteModel(
name="EmailChangeConfirmation",
),
migrations.AlterModelOptions(
name="confirmation",
options={},
),
migrations.AddField(
model_name="confirmation",
name="type",
field=models.PositiveSmallIntegerField(default=1),
preserve_default=False,
),
migrations.AlterField(
model_name="confirmation",
name="confirmation_key",
field=models.CharField(max_length=40),
),
migrations.AlterField(
model_name="confirmation",
name="date_sent",
field=models.DateTimeField(),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 1.11.6 on 2017-11-30 00:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0001_initial"),
("confirmation", "0004_remove_confirmationmanager"),
]
operations = [
migrations.AddField(
model_name="confirmation",
name="realm",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.Realm"
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 1.11.6 on 2018-01-29 18:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0005_confirmation_realm"),
]
operations = [
migrations.AddField(
model_name="realmcreationkey",
name="presume_email_valid",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 2.2.10 on 2020-03-27 09:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0006_realmcreationkey_presume_email_valid"),
]
operations = [
migrations.AlterField(
model_name="confirmation",
name="confirmation_key",
field=models.CharField(db_index=True, max_length=40),
),
migrations.AlterField(
model_name="confirmation",
name="date_sent",
field=models.DateTimeField(db_index=True),
),
migrations.AlterField(
model_name="confirmation",
name="object_id",
field=models.PositiveIntegerField(db_index=True),
),
migrations.AlterField(
model_name="realmcreationkey",
name="creation_key",
field=models.CharField(db_index=True, max_length=40, verbose_name="activation key"),
),
migrations.AlterUniqueTogether(
name="confirmation",
unique_together={("type", "confirmation_key")},
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0007_add_indexes"),
]
operations = [
migrations.AddField(
model_name="confirmation",
name="expiry_date",
field=models.DateTimeField(db_index=True, null=True),
preserve_default=False,
),
]

View File

@@ -0,0 +1,70 @@
# Generated by Django 3.1.7 on 2021-03-31 20:47
import time
from datetime import timedelta
from django.conf import settings
from django.db import migrations, transaction
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
def set_expiry_date_for_existing_confirmations(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Confirmation = apps.get_model("confirmation", "Confirmation")
if not Confirmation.objects.exists():
return
# The values at the time of this migration
INVITATION = 2
UNSUBSCRIBE = 4
MULTIUSE_INVITE = 6
@transaction.atomic
def backfill_confirmations_between(lower_bound: int, upper_bound: int) -> None:
confirmations = Confirmation.objects.filter(id__gte=lower_bound, id__lte=upper_bound)
for confirmation in confirmations:
if confirmation.type in (INVITATION, MULTIUSE_INVITE):
confirmation.expiry_date = confirmation.date_sent + timedelta(
days=settings.INVITATION_LINK_VALIDITY_DAYS
)
elif confirmation.type == UNSUBSCRIBE:
# Unsubscribe links never expire, which we apparently implement as in 1M days.
confirmation.expiry_date = confirmation.date_sent + timedelta(days=1000000)
else:
confirmation.expiry_date = confirmation.date_sent + timedelta(
days=settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS
)
Confirmation.objects.bulk_update(confirmations, ["expiry_date"])
# Because the ranges in this code are inclusive, subtracting 1 offers round numbers.
BATCH_SIZE = 1000 - 1
first_id = Confirmation.objects.earliest("id").id
last_id = Confirmation.objects.latest("id").id
id_range_lower_bound = first_id
id_range_upper_bound = first_id + BATCH_SIZE
while id_range_lower_bound <= last_id:
print(f"Processed {id_range_lower_bound} / {last_id}")
backfill_confirmations_between(id_range_lower_bound, id_range_upper_bound)
id_range_lower_bound = id_range_upper_bound + 1
id_range_upper_bound = id_range_lower_bound + BATCH_SIZE
time.sleep(0.1)
class Migration(migrations.Migration):
atomic = False
dependencies = [
("confirmation", "0008_confirmation_expiry_date"),
]
operations = [
migrations.RunPython(
set_expiry_date_for_existing_confirmations,
reverse_code=migrations.RunPython.noop,
elidable=True,
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.5 on 2021-08-02 19:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0009_confirmation_expiry_date_backfill"),
]
operations = [
migrations.AlterField(
model_name="confirmation",
name="expiry_date",
field=models.DateTimeField(db_index=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.9 on 2021-11-30 17:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0010_alter_confirmation_expiry_date"),
]
operations = [
migrations.AlterField(
model_name="confirmation",
name="expiry_date",
field=models.DateTimeField(db_index=True, null=True),
),
]

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0011_alter_confirmation_expiry_date"),
]
operations = [
migrations.AlterField(
model_name="confirmation",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0012_alter_confirmation_id"),
]
operations = [
migrations.AlterField(
model_name="realmcreationkey",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

View File

@@ -0,0 +1,20 @@
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
("confirmation", "0013_alter_realmcreationkey_id"),
("contenttypes", "0002_remove_content_type_name"),
]
operations = [
AddIndexConcurrently(
model_name="confirmation",
index=models.Index(
fields=["content_type", "object_id"], name="confirmatio_content_80155a_idx"
),
),
]

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0014_confirmation_confirmatio_content_80155a_idx"),
# We want to be linking to tables that are already bigints
("zerver", "0531_convert_most_ids_to_bigints"),
]
operations = [
migrations.AlterField(
model_name="confirmation",
name="object_id",
field=models.PositiveBigIntegerField(db_index=True),
),
]

View File

@@ -0,0 +1,112 @@
# Generated by Django 5.2.3 on 2025-07-04 20:33
from datetime import timedelta
from django.conf import settings
from django.db import migrations, transaction
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import Max
def migrate_realmcreationkey_to_realmcreationstatus(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
"""
This migration is for switching from using a separate RealmCreationKey class
for realm creation keys to just using the Confirmation system.
The aim is to iterate through all the existing RealmCreationKey and create
a corresponding Confirmation+RealmCreationStatus.
For validity of these objects, we only need to worry about RealmCreationKeys
expired due to time. This is taken care of by making sure we set expiry_date
on the Confirmation we're creating.
The way the RealmCreationKey system worked was to .delete() the RealmCreationKey
objects when they were used - so those simply no longer exist and we don't need to
worry about this case.
"""
CAN_CREATE_REALM = 11
Confirmation = apps.get_model("confirmation", "Confirmation")
ContentType = apps.get_model("contenttypes", "ContentType")
RealmCreationKey = apps.get_model("confirmation", "RealmCreationKey")
RealmCreationStatus = apps.get_model("zerver", "RealmCreationStatus")
realm_creation_status_content_type, _created = ContentType.objects.get_or_create(
model="realmcreationstatus", app_label="zerver"
)
BATCH_SIZE = 10000
max_id = RealmCreationKey.objects.aggregate(Max("id"))["id__max"]
if max_id is None:
# Nothing to do.
return
lower_bound = 1
while lower_bound <= max_id + (BATCH_SIZE / 2):
upper_bound = lower_bound + BATCH_SIZE - 1
creation_keys = RealmCreationKey.objects.filter(id__range=(lower_bound, upper_bound))
creation_statuses_to_create = []
confirmations_to_create = []
for creation_key in creation_keys:
# These keys were generated in the same way as keys for Confirmation objects,
# so we can copy them over without breaking anything.
key = creation_key.creation_key
date_created = creation_key.date_created
presume_email_valid = creation_key.presume_email_valid
creation_status = RealmCreationStatus(
status=0, date_created=date_created, presume_email_valid=presume_email_valid
)
confirmation = Confirmation(
content_type=realm_creation_status_content_type,
type=CAN_CREATE_REALM,
confirmation_key=key,
date_sent=date_created,
expiry_date=date_created
+ timedelta(days=settings.CAN_CREATE_REALM_LINK_VALIDITY_DAYS),
)
# To attach the Confirmations to RealmCreationStatus objects we need to set the
# confirmation.object_id to their respective ids. But we haven't saved
# the RealmCreationStatus objs to the database yet - so we don't have ids.
#
# After we .bulk_create() them, their .id attributes will be populated.
# So for now we just link the RealmCreationStatus to the Confirmation
# via a temporary ._object attribute - which we'll use later to set
# the .object_id as intended.
confirmation._object = creation_status
creation_statuses_to_create.append(creation_status)
confirmations_to_create.append(confirmation)
with transaction.atomic():
RealmCreationStatus.objects.bulk_create(creation_statuses_to_create)
# Now the objects in creation_statuses_to_create have had their .id
# attrs populated. For every confirmation, confirmation._object
# points to its corresponding RealmCreationStatus - so now we can
# set the confirmation.object_id values and clear out the temporary
# ._object attr.
for confirmation in confirmations_to_create:
confirmation.object_id = confirmation._object.id
delattr(confirmation, "_object")
Confirmation.objects.bulk_create(confirmations_to_create)
lower_bound += BATCH_SIZE
class Migration(migrations.Migration):
atomic = False
dependencies = [
("confirmation", "0015_alter_confirmation_object_id"),
("zerver", "0747_realmcreationstatus"),
]
operations = [
migrations.RunPython(
migrate_realmcreationkey_to_realmcreationstatus,
reverse_code=migrations.RunPython.noop,
elidable=True,
),
]

View File

@@ -0,0 +1,15 @@
# Generated by Django 5.2.3 on 2025-07-04 19:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("confirmation", "0016_realmcreationkey_to_realmcreationstatus"),
]
operations = [
migrations.DeleteModel(
name="RealmCreationKey",
),
]

View File

308
confirmation/models.py Normal file
View File

@@ -0,0 +1,308 @@
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
__revision__ = "$Id: models.py 28 2009-10-22 15:03:02Z jarek.zgoda $"
import secrets
from base64 import b32encode
from collections.abc import Mapping
from datetime import timedelta
from typing import TypeAlias, Union, cast
from urllib.parse import urljoin
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import CASCADE
from django.http import HttpRequest, HttpResponse
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.timezone import now as timezone_now
from typing_extensions import override
from confirmation import settings as confirmation_settings
from zerver.lib.types import UNSET, Unset
from zerver.models import (
EmailChangeStatus,
MultiuseInvite,
PreregistrationRealm,
PreregistrationUser,
Realm,
RealmReactivationStatus,
UserProfile,
)
from zerver.models.prereg_users import RealmCreationStatus
if settings.ZILENCER_ENABLED:
from zilencer.models import (
PreregistrationRemoteRealmBillingUser,
PreregistrationRemoteServerBillingUser,
)
class ConfirmationKeyError(Exception):
WRONG_LENGTH = 1
EXPIRED = 2
DOES_NOT_EXIST = 3
def __init__(self, error_type: int) -> None:
super().__init__()
self.error_type = error_type
def render_confirmation_key_error(
request: HttpRequest, exception: ConfirmationKeyError
) -> HttpResponse:
if exception.error_type == ConfirmationKeyError.WRONG_LENGTH:
return TemplateResponse(request, "confirmation/link_malformed.html", status=404)
if exception.error_type == ConfirmationKeyError.EXPIRED:
return TemplateResponse(request, "confirmation/link_expired.html", status=404)
return TemplateResponse(request, "confirmation/link_does_not_exist.html", status=404)
def generate_key() -> str:
# 24 characters * 5 bits of entropy/character = 120 bits of entropy
return b32encode(secrets.token_bytes(15)).decode().lower()
NoZilencerConfirmationObjT: TypeAlias = (
MultiuseInvite
| PreregistrationRealm
| PreregistrationUser
| EmailChangeStatus
| UserProfile
| RealmReactivationStatus
| RealmCreationStatus
)
ZilencerConfirmationObjT: TypeAlias = Union[
NoZilencerConfirmationObjT,
"PreregistrationRemoteServerBillingUser",
"PreregistrationRemoteRealmBillingUser",
]
ConfirmationObjT: TypeAlias = NoZilencerConfirmationObjT | ZilencerConfirmationObjT
def get_object_from_key(
confirmation_key: str,
confirmation_types: list[int],
*,
mark_object_used: bool,
allow_used: bool = False,
) -> ConfirmationObjT:
"""Access a confirmation object from one of the provided confirmation
types with the provided key.
The mark_object_used parameter determines whether to mark the
confirmation object as used (which generally prevents it from
being used again). It should always be False for MultiuseInvite
objects, since they are intended to be used multiple times.
By default, used confirmation objects cannot be used again as part
of their security model.
"""
# Confirmation keys used to be 40 characters
if len(confirmation_key) not in (24, 40):
raise ConfirmationKeyError(ConfirmationKeyError.WRONG_LENGTH)
try:
confirmation = Confirmation.objects.get(
confirmation_key=confirmation_key, type__in=confirmation_types
)
except Confirmation.DoesNotExist:
raise ConfirmationKeyError(ConfirmationKeyError.DOES_NOT_EXIST)
if confirmation.expiry_date is not None and timezone_now() > confirmation.expiry_date:
raise ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
obj = confirmation.content_object
assert obj is not None
forbidden_statuses = {confirmation_settings.STATUS_REVOKED}
if not allow_used:
forbidden_statuses.add(confirmation_settings.STATUS_USED)
if hasattr(obj, "status") and obj.status in forbidden_statuses:
# Confirmations where the object has the status attribute are one-time use
# and are marked after being revoked (or used).
raise ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
if mark_object_used:
# 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")
obj.status = getattr(settings, "STATUS_USED", 1)
obj.save(update_fields=["status"])
return obj
def create_confirmation_object(
obj: ConfirmationObjT,
confirmation_type: int,
*,
validity_in_minutes: int | Unset | None = UNSET,
no_associated_realm_object: bool = False,
) -> "Confirmation":
# validity_in_minutes is an override for the default values which are
# determined by the confirmation_type - its main purpose is for use
# in tests which may want to have control over the exact expiration time.
key = generate_key()
# Some confirmation objects, like those for realm creation or those used
# for the self-hosted management flows, are not associated with a realm
# hosted by this Zulip server.
if no_associated_realm_object:
realm = None
else:
obj = cast(NoZilencerConfirmationObjT, obj)
assert not isinstance(obj, PreregistrationRealm | RealmCreationStatus)
realm = obj.realm
current_time = timezone_now()
expiry_date = None
if not isinstance(validity_in_minutes, Unset):
if validity_in_minutes is None:
expiry_date = None
else:
assert validity_in_minutes is not None
expiry_date = current_time + timedelta(minutes=validity_in_minutes)
else:
expiry_date = current_time + timedelta(days=_properties[confirmation_type].validity_in_days)
return Confirmation.objects.create(
content_object=obj,
date_sent=current_time,
confirmation_key=key,
realm=realm,
expiry_date=expiry_date,
type=confirmation_type,
)
def create_confirmation_link(
obj: ConfirmationObjT,
confirmation_type: int,
*,
validity_in_minutes: int | Unset | None = UNSET,
url_args: Mapping[str, str] = {},
no_associated_realm_object: bool = False,
) -> str:
conf = create_confirmation_object(
obj,
confirmation_type,
validity_in_minutes=validity_in_minutes,
no_associated_realm_object=no_associated_realm_object,
)
result = confirmation_url_for(
conf,
url_args=url_args,
)
return result
def confirmation_url_for(confirmation_obj: "Confirmation", url_args: Mapping[str, str] = {}) -> str:
return confirmation_url(
confirmation_obj.confirmation_key, confirmation_obj.realm, confirmation_obj.type, url_args
)
def confirmation_url(
confirmation_key: str,
realm: Realm | None,
confirmation_type: int,
url_args: Mapping[str, str] = {},
) -> str:
url_args = dict(url_args)
url_args["confirmation_key"] = confirmation_key
return urljoin(
settings.ROOT_DOMAIN_URI if realm is None else realm.url,
reverse(_properties[confirmation_type].url_name, kwargs=url_args),
)
class Confirmation(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=CASCADE)
object_id = models.PositiveBigIntegerField(db_index=True)
content_object = GenericForeignKey("content_type", "object_id")
date_sent = models.DateTimeField(db_index=True)
confirmation_key = models.CharField(max_length=40, db_index=True)
expiry_date = models.DateTimeField(db_index=True, null=True)
realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE)
# The following list is the set of valid types
USER_REGISTRATION = 1
INVITATION = 2
EMAIL_CHANGE = 3
UNSUBSCRIBE = 4
SERVER_REGISTRATION = 5
MULTIUSE_INVITE = 6
NEW_REALM_USER_REGISTRATION = 7
REALM_REACTIVATION = 8
REMOTE_SERVER_BILLING_LEGACY_LOGIN = 9
REMOTE_REALM_BILLING_LEGACY_LOGIN = 10
CAN_CREATE_REALM = 11
type = models.PositiveSmallIntegerField()
class Meta:
unique_together = ("type", "confirmation_key")
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
@override
def __str__(self) -> str:
return f"{self.content_object!r}"
class ConfirmationType:
def __init__(
self,
url_name: str,
validity_in_days: int = settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS,
) -> None:
self.url_name = url_name
self.validity_in_days = validity_in_days
_properties = {
Confirmation.USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
Confirmation.INVITATION: ConfirmationType(
"get_prereg_key_and_redirect", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
),
Confirmation.EMAIL_CHANGE: ConfirmationType("confirm_email_change_get"),
Confirmation.UNSUBSCRIBE: ConfirmationType(
"unsubscribe",
validity_in_days=1000000, # should never expire
),
Confirmation.MULTIUSE_INVITE: ConfirmationType(
"join", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
),
Confirmation.CAN_CREATE_REALM: ConfirmationType(
"create_realm", validity_in_days=settings.CAN_CREATE_REALM_LINK_VALIDITY_DAYS
),
Confirmation.NEW_REALM_USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation_get"),
}
if settings.ZILENCER_ENABLED:
_properties[Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN] = ConfirmationType(
"remote_billing_legacy_server_from_login_confirmation_link"
)
_properties[Confirmation.REMOTE_REALM_BILLING_LEGACY_LOGIN] = ConfirmationType(
"remote_realm_billing_from_login_confirmation_link"
)
def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str:
"""
Generate a unique link that a logged-out user can visit to unsubscribe from
Zulip e-mails without having to first log in.
"""
return create_confirmation_link(
user_profile, Confirmation.UNSUBSCRIBE, url_args={"email_type": email_type}
)
def generate_realm_creation_url(by_admin: bool = False) -> str:
from zerver.views.registration import prepare_realm_creation_url
return prepare_realm_creation_url(presume_email_valid=by_admin)

6
confirmation/settings.py Normal file
View File

@@ -0,0 +1,6 @@
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
__revision__ = "$Id: settings.py 12 2008-11-23 19:38:52Z jarek.zgoda $"
STATUS_USED = 1
STATUS_REVOKED = 2