2012-09-28 22:29:48 +02:00
|
|
|
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
__revision__ = "$Id: models.py 28 2009-10-22 15:03:02Z jarek.zgoda $"
|
2020-09-05 04:02:13 +02:00
|
|
|
import secrets
|
|
|
|
from base64 import b32encode
|
2023-11-19 19:45:19 +01:00
|
|
|
from datetime import timedelta
|
2022-06-28 21:53:19 +02:00
|
|
|
from typing import List, Mapping, Optional, Union
|
2020-06-14 01:36:12 +02:00
|
|
|
from urllib.parse import urljoin
|
2012-09-28 22:29:48 +02:00
|
|
|
|
|
|
|
from django.conf import settings
|
2016-10-10 14:52:01 +02:00
|
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
from django.db import models
|
|
|
|
from django.db.models import CASCADE
|
2017-07-22 00:25:41 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2023-02-01 08:05:01 +01:00
|
|
|
from django.template.response import TemplateResponse
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.urls import reverse
|
2017-04-15 04:03:56 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2023-10-12 19:43:45 +02:00
|
|
|
from typing_extensions import TypeAlias, override
|
2012-09-28 22:29:48 +02:00
|
|
|
|
2022-07-25 19:55:35 +02:00
|
|
|
from confirmation import settings as confirmation_settings
|
2021-11-30 13:34:37 +01:00
|
|
|
from zerver.lib.types import UnspecifiedValue
|
2022-07-27 22:04:14 +02:00
|
|
|
from zerver.models import (
|
|
|
|
EmailChangeStatus,
|
|
|
|
MultiuseInvite,
|
2023-03-03 11:58:00 +01:00
|
|
|
PreregistrationRealm,
|
2022-07-27 22:04:14 +02:00
|
|
|
PreregistrationUser,
|
|
|
|
Realm,
|
|
|
|
RealmReactivationStatus,
|
|
|
|
UserProfile,
|
|
|
|
)
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2012-09-28 22:29:48 +02:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
class ConfirmationKeyError(Exception):
|
2017-07-22 00:24:42 +02:00
|
|
|
WRONG_LENGTH = 1
|
|
|
|
EXPIRED = 2
|
|
|
|
DOES_NOT_EXIST = 3
|
|
|
|
|
2017-10-27 10:52:58 +02:00
|
|
|
def __init__(self, error_type: int) -> None:
|
2017-10-27 08:28:23 +02:00
|
|
|
super().__init__()
|
2017-07-22 00:24:42 +02:00
|
|
|
self.error_type = error_type
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def render_confirmation_key_error(
|
2022-11-17 09:30:48 +01:00
|
|
|
request: HttpRequest, exception: ConfirmationKeyError
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2022-11-17 09:30:48 +01:00
|
|
|
if exception.error_type == ConfirmationKeyError.WRONG_LENGTH:
|
2023-02-01 08:05:01 +01:00
|
|
|
return TemplateResponse(request, "confirmation/link_malformed.html", status=404)
|
2022-11-17 09:30:48 +01:00
|
|
|
if exception.error_type == ConfirmationKeyError.EXPIRED:
|
2023-02-01 08:05:01 +01:00
|
|
|
return TemplateResponse(request, "confirmation/link_expired.html", status=404)
|
|
|
|
return TemplateResponse(request, "confirmation/link_does_not_exist.html", status=404)
|
2017-07-22 00:25:41 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-10-27 10:52:58 +02:00
|
|
|
def generate_key() -> str:
|
2017-07-11 20:52:27 +02:00
|
|
|
# 24 characters * 5 bits of entropy/character = 120 bits of entropy
|
2020-09-05 04:02:13 +02:00
|
|
|
return b32encode(secrets.token_bytes(15)).decode().lower()
|
2013-02-28 20:07:04 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-08-02 23:53:10 +02:00
|
|
|
ConfirmationObjT: TypeAlias = Union[
|
2022-07-27 22:04:14 +02:00
|
|
|
MultiuseInvite,
|
2023-03-03 11:58:00 +01:00
|
|
|
PreregistrationRealm,
|
2022-07-27 22:04:14 +02:00
|
|
|
PreregistrationUser,
|
|
|
|
EmailChangeStatus,
|
|
|
|
UserProfile,
|
|
|
|
RealmReactivationStatus,
|
|
|
|
]
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
def get_object_from_key(
|
2022-07-21 15:26:09 +02:00
|
|
|
confirmation_key: str, confirmation_types: List[int], *, mark_object_used: bool
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> ConfirmationObjT:
|
2022-07-19 21:13:32 +02:00
|
|
|
"""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.
|
|
|
|
"""
|
|
|
|
|
2017-07-22 00:27:45 +02:00
|
|
|
# Confirmation keys used to be 40 characters
|
|
|
|
if len(confirmation_key) not in (24, 40):
|
2022-11-17 09:30:48 +01:00
|
|
|
raise ConfirmationKeyError(ConfirmationKeyError.WRONG_LENGTH)
|
2017-07-08 06:36:39 +02:00
|
|
|
try:
|
2021-02-12 08:19:30 +01:00
|
|
|
confirmation = Confirmation.objects.get(
|
2021-12-02 16:34:05 +01:00
|
|
|
confirmation_key=confirmation_key, type__in=confirmation_types
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2017-07-08 06:36:39 +02:00
|
|
|
except Confirmation.DoesNotExist:
|
2022-11-17 09:30:48 +01:00
|
|
|
raise ConfirmationKeyError(ConfirmationKeyError.DOES_NOT_EXIST)
|
2017-07-08 06:36:39 +02:00
|
|
|
|
2021-11-30 13:34:37 +01:00
|
|
|
if confirmation.expiry_date is not None and timezone_now() > confirmation.expiry_date:
|
2022-11-17 09:30:48 +01:00
|
|
|
raise ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
|
2017-07-08 06:36:39 +02:00
|
|
|
|
|
|
|
obj = confirmation.content_object
|
2021-07-24 18:16:48 +02:00
|
|
|
assert obj is not None
|
2022-07-19 21:13:32 +02:00
|
|
|
|
2022-07-25 19:55:35 +02:00
|
|
|
used_value = confirmation_settings.STATUS_USED
|
|
|
|
revoked_value = confirmation_settings.STATUS_REVOKED
|
|
|
|
if hasattr(obj, "status") and obj.status in [used_value, revoked_value]:
|
|
|
|
# Confirmations where the object has the status attribute are one-time use
|
|
|
|
# and are marked after being used (or revoked).
|
2022-11-17 09:30:48 +01:00
|
|
|
raise ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
|
2022-07-25 19:55:35 +02:00
|
|
|
|
2022-07-19 21:13:32 +02:00
|
|
|
if mark_object_used:
|
2022-09-12 00:39:43 +02:00
|
|
|
# MultiuseInvite objects do not use the STATUS_USED status, since they are
|
2022-07-19 21:13:32 +02:00
|
|
|
# intended to be used more than once.
|
|
|
|
assert confirmation.type != Confirmation.MULTIUSE_INVITE
|
|
|
|
assert hasattr(obj, "status")
|
2022-07-16 20:09:13 +02:00
|
|
|
obj.status = getattr(settings, "STATUS_USED", 1)
|
2021-02-12 08:20:45 +01:00
|
|
|
obj.save(update_fields=["status"])
|
2017-07-08 06:36:39 +02:00
|
|
|
return obj
|
2017-07-08 04:18:58 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def create_confirmation_link(
|
2022-06-28 21:53:19 +02:00
|
|
|
obj: ConfirmationObjT,
|
2021-07-24 19:40:01 +02:00
|
|
|
confirmation_type: int,
|
2021-07-31 22:08:54 +02:00
|
|
|
*,
|
2022-02-10 11:52:34 +01:00
|
|
|
validity_in_minutes: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(),
|
2021-07-24 19:40:01 +02:00
|
|
|
url_args: Mapping[str, str] = {},
|
2023-03-03 11:58:00 +01:00
|
|
|
realm_creation: bool = False,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> str:
|
2022-02-10 11:52:34 +01:00
|
|
|
# validity_in_minutes is an override for the default values which are
|
2021-07-31 22:08:54 +02:00
|
|
|
# determined by the confirmation_type - its main purpose is for use
|
|
|
|
# in tests which may want to have control over the exact expiration time.
|
2017-07-08 04:38:13 +02:00
|
|
|
key = generate_key()
|
2023-03-03 11:58:00 +01:00
|
|
|
if realm_creation:
|
|
|
|
realm = None
|
|
|
|
else:
|
|
|
|
assert not isinstance(obj, PreregistrationRealm)
|
|
|
|
realm = obj.realm
|
2019-09-17 14:04:48 +02:00
|
|
|
|
2021-09-11 01:54:09 +02:00
|
|
|
current_time = timezone_now()
|
2021-08-02 20:45:55 +02:00
|
|
|
expiry_date = None
|
2022-02-10 11:52:34 +01:00
|
|
|
if not isinstance(validity_in_minutes, UnspecifiedValue):
|
|
|
|
if validity_in_minutes is None:
|
2021-11-30 13:34:37 +01:00
|
|
|
expiry_date = None
|
|
|
|
else:
|
2022-02-10 11:52:34 +01:00
|
|
|
assert validity_in_minutes is not None
|
2023-11-19 19:45:19 +01:00
|
|
|
expiry_date = current_time + timedelta(minutes=validity_in_minutes)
|
2021-08-02 20:45:55 +02:00
|
|
|
else:
|
2023-11-19 19:45:19 +01:00
|
|
|
expiry_date = current_time + timedelta(days=_properties[confirmation_type].validity_in_days)
|
2021-08-02 20:45:55 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
Confirmation.objects.create(
|
|
|
|
content_object=obj,
|
2021-09-11 01:54:09 +02:00
|
|
|
date_sent=current_time,
|
2021-02-12 08:19:30 +01:00
|
|
|
confirmation_key=key,
|
|
|
|
realm=realm,
|
2021-08-02 20:45:55 +02:00
|
|
|
expiry_date=expiry_date,
|
2021-02-12 08:19:30 +01:00
|
|
|
type=confirmation_type,
|
|
|
|
)
|
2020-06-14 01:36:12 +02:00
|
|
|
return confirmation_url(key, realm, confirmation_type, url_args)
|
2017-07-08 04:38:13 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def confirmation_url(
|
|
|
|
confirmation_key: str,
|
|
|
|
realm: Optional[Realm],
|
|
|
|
confirmation_type: int,
|
|
|
|
url_args: Mapping[str, str] = {},
|
|
|
|
) -> str:
|
2020-06-13 03:34:01 +02:00
|
|
|
url_args = dict(url_args)
|
2021-02-12 08:20:45 +01:00
|
|
|
url_args["confirmation_key"] = confirmation_key
|
2020-06-14 01:36:12 +02:00
|
|
|
return urljoin(
|
|
|
|
settings.ROOT_DOMAIN_URI if realm is None else realm.uri,
|
|
|
|
reverse(_properties[confirmation_type].url_name, kwargs=url_args),
|
|
|
|
)
|
2017-01-17 11:11:51 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2012-09-28 22:29:48 +02:00
|
|
|
class Confirmation(models.Model):
|
2018-01-29 08:17:31 +01:00
|
|
|
content_type = models.ForeignKey(ContentType, on_delete=CASCADE)
|
2022-08-15 19:10:58 +02:00
|
|
|
object_id = models.PositiveIntegerField(db_index=True)
|
2021-02-12 08:20:45 +01:00
|
|
|
content_object = GenericForeignKey("content_type", "object_id")
|
2022-08-15 19:10:58 +02:00
|
|
|
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)
|
2012-09-28 22:29:48 +02:00
|
|
|
|
2017-07-08 06:25:05 +02:00
|
|
|
# The following list is the set of valid types
|
2017-07-08 04:38:13 +02:00
|
|
|
USER_REGISTRATION = 1
|
|
|
|
INVITATION = 2
|
|
|
|
EMAIL_CHANGE = 3
|
|
|
|
UNSUBSCRIBE = 4
|
|
|
|
SERVER_REGISTRATION = 5
|
2017-08-10 22:34:17 +02:00
|
|
|
MULTIUSE_INVITE = 6
|
2017-11-30 01:06:25 +01:00
|
|
|
REALM_CREATION = 7
|
2018-11-12 14:15:49 +01:00
|
|
|
REALM_REACTIVATION = 8
|
2022-08-15 19:10:58 +02:00
|
|
|
type = models.PositiveSmallIntegerField()
|
2012-09-28 22:29:48 +02:00
|
|
|
|
2020-03-27 10:03:05 +01:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("type", "confirmation_key")
|
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2023-04-12 22:40:35 +02:00
|
|
|
def __str__(self) -> str:
|
|
|
|
return f"{self.content_object!r}"
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-05 11:57:15 +01:00
|
|
|
class ConfirmationType:
|
2021-02-12 08:19:30 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
url_name: str,
|
|
|
|
validity_in_days: int = settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS,
|
|
|
|
) -> None:
|
2017-07-08 04:38:13 +02:00
|
|
|
self.url_name = url_name
|
2017-07-08 06:50:57 +02:00
|
|
|
self.validity_in_days = validity_in_days
|
2017-07-08 04:38:13 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-07-08 04:38:13 +02:00
|
|
|
_properties = {
|
2021-11-29 16:20:59 +01:00
|
|
|
Confirmation.USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
|
2021-02-12 08:19:30 +01:00
|
|
|
Confirmation.INVITATION: ConfirmationType(
|
2021-11-29 16:20:59 +01:00
|
|
|
"get_prereg_key_and_redirect", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2021-02-12 08:20:45 +01:00
|
|
|
Confirmation.EMAIL_CHANGE: ConfirmationType("confirm_email_change"),
|
2020-09-02 02:50:08 +02:00
|
|
|
Confirmation.UNSUBSCRIBE: ConfirmationType(
|
2021-02-12 08:20:45 +01:00
|
|
|
"unsubscribe",
|
2020-09-02 02:50:08 +02:00
|
|
|
validity_in_days=1000000, # should never expire
|
|
|
|
),
|
2017-11-10 04:33:28 +01:00
|
|
|
Confirmation.MULTIUSE_INVITE: ConfirmationType(
|
2021-02-12 08:20:45 +01:00
|
|
|
"join", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2021-11-29 16:20:59 +01:00
|
|
|
Confirmation.REALM_CREATION: ConfirmationType("get_prereg_key_and_redirect"),
|
2021-02-12 08:20:45 +01:00
|
|
|
Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation"),
|
2017-07-08 04:38:13 +02:00
|
|
|
}
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-11-08 22:40:27 +01:00
|
|
|
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.
|
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
return create_confirmation_link(
|
2021-02-12 08:20:45 +01:00
|
|
|
user_profile, Confirmation.UNSUBSCRIBE, url_args={"email_type": email_type}
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
|
2018-11-08 22:40:27 +01:00
|
|
|
|
2017-11-30 00:48:34 +01:00
|
|
|
# Functions related to links generated by the generate_realm_creation_link.py
|
|
|
|
# management command.
|
|
|
|
# Note that being validated here will just allow the user to access the create_realm
|
|
|
|
# form, where they will enter their email and go through the regular
|
|
|
|
# Confirmation.REALM_CREATION pathway.
|
|
|
|
# Arguably RealmCreationKey should just be another ConfirmationObjT and we should
|
|
|
|
# add another Confirmation.type for this; it's this way for historical reasons.
|
2017-07-06 06:59:17 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
def validate_key(creation_key: Optional[str]) -> Optional["RealmCreationKey"]:
|
2018-01-29 20:54:49 +01:00
|
|
|
"""Get the record for this key, raising InvalidCreationKey if non-None but invalid."""
|
|
|
|
if creation_key is None:
|
|
|
|
return None
|
|
|
|
try:
|
|
|
|
key_record = RealmCreationKey.objects.get(creation_key=creation_key)
|
|
|
|
except RealmCreationKey.DoesNotExist:
|
2023-02-04 02:07:20 +01:00
|
|
|
raise RealmCreationKey.InvalidError
|
2018-01-29 20:54:49 +01:00
|
|
|
time_elapsed = timezone_now() - key_record.date_created
|
2017-11-30 00:19:01 +01:00
|
|
|
if time_elapsed.total_seconds() > settings.REALM_CREATION_LINK_VALIDITY_DAYS * 24 * 3600:
|
2023-02-04 02:07:20 +01:00
|
|
|
raise RealmCreationKey.InvalidError
|
2018-01-29 20:54:49 +01:00
|
|
|
return key_record
|
2017-07-06 06:59:17 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def generate_realm_creation_url(by_admin: bool = False) -> str:
|
2017-07-06 06:59:17 +02:00
|
|
|
key = generate_key()
|
2021-02-12 08:19:30 +01:00
|
|
|
RealmCreationKey.objects.create(
|
|
|
|
creation_key=key, date_created=timezone_now(), presume_email_valid=by_admin
|
|
|
|
)
|
2020-06-14 01:36:12 +02:00
|
|
|
return urljoin(
|
|
|
|
settings.ROOT_DOMAIN_URI,
|
2021-02-12 08:20:45 +01:00
|
|
|
reverse("create_realm", kwargs={"creation_key": key}),
|
2020-06-14 01:36:12 +02:00
|
|
|
)
|
2017-07-06 06:59:17 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2016-06-22 21:16:02 +02:00
|
|
|
class RealmCreationKey(models.Model):
|
2021-02-12 08:20:45 +01:00
|
|
|
creation_key = models.CharField("activation key", db_index=True, max_length=40)
|
|
|
|
date_created = models.DateTimeField("created", default=timezone_now)
|
2018-01-29 19:58:00 +01:00
|
|
|
|
|
|
|
# True just if we should presume the email address the user enters
|
|
|
|
# is theirs, and skip sending mail to it to confirm that.
|
2022-08-15 19:10:58 +02:00
|
|
|
presume_email_valid = models.BooleanField(default=False)
|
2018-01-29 20:54:49 +01:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
class InvalidError(Exception):
|
2018-01-29 20:54:49 +01:00
|
|
|
pass
|