2020-06-11 00:54:34 +02:00
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
|
|
import DNS
|
2012-08-28 18:44:51 +02:00
|
|
|
from django import forms
|
2016-11-05 04:13:40 +01:00
|
|
|
from django.conf import settings
|
2020-05-28 11:22:45 +02:00
|
|
|
from django.contrib.auth import authenticate, password_validation
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm
|
|
|
|
from django.contrib.auth.tokens import PasswordResetTokenGenerator, default_token_generator
|
2016-11-05 04:13:40 +01:00
|
|
|
from django.core.exceptions import ValidationError
|
2016-12-20 10:41:46 +01:00
|
|
|
from django.core.validators import validate_email
|
2017-10-04 07:30:17 +02:00
|
|
|
from django.http import HttpRequest
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.urls import reverse
|
|
|
|
from django.utils.encoding import force_bytes
|
|
|
|
from django.utils.http import urlsafe_base64_encode
|
|
|
|
from django.utils.translation import ugettext as _
|
2016-11-05 04:13:40 +01:00
|
|
|
from jinja2 import Markup as mark_safe
|
2020-06-11 00:54:34 +02:00
|
|
|
from two_factor.forms import AuthenticationTokenForm as TwoFactorAuthenticationTokenForm
|
|
|
|
from two_factor.utils import totp_digits
|
2016-11-05 04:13:40 +01:00
|
|
|
|
2020-03-05 16:56:08 +01:00
|
|
|
from zerver.lib.actions import do_change_password, email_not_system_bot
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
|
|
|
|
from zerver.lib.name_restrictions import is_disposable_domain, is_reserved_subdomain
|
2020-04-01 13:13:06 +02:00
|
|
|
from zerver.lib.rate_limiter import RateLimited, RateLimitedObject
|
2017-02-08 05:04:14 +01:00
|
|
|
from zerver.lib.request import JsonableError
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.send_email import FromAddress, send_email
|
2019-02-02 23:53:55 +01:00
|
|
|
from zerver.lib.subdomains import get_subdomain, is_root_domain_available
|
2017-02-08 05:04:14 +01:00
|
|
|
from zerver.lib.users import check_full_name
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.models import (
|
|
|
|
DisposableEmailError,
|
|
|
|
DomainNotAllowedForRealmError,
|
|
|
|
EmailContainsPlusError,
|
|
|
|
Realm,
|
|
|
|
UserProfile,
|
|
|
|
email_to_domain,
|
|
|
|
get_realm,
|
|
|
|
get_user_by_delivery_email,
|
|
|
|
)
|
|
|
|
from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap
|
2012-09-26 20:08:39 +02:00
|
|
|
|
2020-04-09 21:51:58 +02:00
|
|
|
MIT_VALIDATION_ERROR = 'That user does not exist at MIT or is a ' + \
|
|
|
|
'<a href="https://ist.mit.edu/email-lists">mailing list</a>. ' + \
|
|
|
|
'If you want to sign up an alias for Zulip, ' + \
|
2020-05-28 02:00:13 +02:00
|
|
|
'<a href="mailto:support@zulip.com">contact us</a>.'
|
2016-07-19 14:35:08 +02:00
|
|
|
WRONG_SUBDOMAIN_ERROR = "Your Zulip account is not a member of the " + \
|
|
|
|
"organization associated with this subdomain. " + \
|
2019-06-26 23:09:20 +02:00
|
|
|
"Please contact your organization administrator with any questions."
|
2020-04-09 21:51:58 +02:00
|
|
|
DEACTIVATED_ACCOUNT_ERROR = "Your account is no longer active. " + \
|
|
|
|
"Please contact your organization administrator to reactivate it."
|
|
|
|
PASSWORD_TOO_WEAK_ERROR = "The password is too weak."
|
2019-12-30 02:21:51 +01:00
|
|
|
AUTHENTICATION_RATE_LIMITED_ERROR = "You're making too many attempts to sign in. " + \
|
|
|
|
"Try again in %s seconds or contact your organization administrator " + \
|
|
|
|
"for help."
|
2016-06-03 01:02:58 +02:00
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def email_is_not_mit_mailing_list(email: str) -> None:
|
2016-07-27 02:39:14 +02:00
|
|
|
"""Prevent MIT mailing lists from signing up for Zulip"""
|
2016-11-06 00:29:55 +01:00
|
|
|
if "@mit.edu" in email:
|
|
|
|
username = email.rsplit("@", 1)[0]
|
2013-08-12 00:47:28 +02:00
|
|
|
# Check whether the user exists and can get mail.
|
|
|
|
try:
|
2020-06-10 06:41:04 +02:00
|
|
|
DNS.dnslookup(f"{username}.pobox.ns.athena.mit.edu", DNS.Type.TXT)
|
2015-11-01 17:08:33 +01:00
|
|
|
except DNS.Base.ServerError as e:
|
2013-08-12 00:47:28 +02:00
|
|
|
if e.rcode == DNS.Status.NXDOMAIN:
|
2016-06-23 23:34:37 +02:00
|
|
|
raise ValidationError(mark_safe(MIT_VALIDATION_ERROR))
|
2013-08-12 00:47:28 +02:00
|
|
|
else:
|
2017-11-18 02:25:44 +01:00
|
|
|
raise AssertionError("Unexpected DNS error")
|
2013-08-12 00:47:28 +02:00
|
|
|
|
2018-04-23 20:02:45 +02:00
|
|
|
def check_subdomain_available(subdomain: str, from_management_command: bool=False) -> None:
|
2017-10-04 02:43:55 +02:00
|
|
|
error_strings = {
|
|
|
|
'too short': _("Subdomain needs to have length 3 or greater."),
|
|
|
|
'extremal dash': _("Subdomain cannot start or end with a '-'."),
|
|
|
|
'bad character': _("Subdomain can only have lowercase letters, numbers, and '-'s."),
|
|
|
|
'unavailable': _("Subdomain unavailable. Please choose a different one.")}
|
|
|
|
|
2017-10-19 08:30:40 +02:00
|
|
|
if subdomain == Realm.SUBDOMAIN_FOR_ROOT_DOMAIN:
|
|
|
|
if is_root_domain_available():
|
|
|
|
return
|
|
|
|
raise ValidationError(error_strings['unavailable'])
|
2017-10-04 02:43:55 +02:00
|
|
|
if subdomain[0] == '-' or subdomain[-1] == '-':
|
|
|
|
raise ValidationError(error_strings['extremal dash'])
|
|
|
|
if not re.match('^[a-z0-9-]*$', subdomain):
|
|
|
|
raise ValidationError(error_strings['bad character'])
|
2018-04-23 20:02:45 +02:00
|
|
|
if from_management_command:
|
|
|
|
return
|
2018-01-25 19:30:40 +01:00
|
|
|
if len(subdomain) < 3:
|
|
|
|
raise ValidationError(error_strings['too short'])
|
2017-10-04 02:43:55 +02:00
|
|
|
if is_reserved_subdomain(subdomain) or \
|
2019-05-04 04:47:44 +02:00
|
|
|
Realm.objects.filter(string_id=subdomain).exists():
|
2017-10-04 02:43:55 +02:00
|
|
|
raise ValidationError(error_strings['unavailable'])
|
|
|
|
|
2012-08-28 18:44:51 +02:00
|
|
|
class RegistrationForm(forms.Form):
|
2017-03-23 00:15:06 +01:00
|
|
|
MAX_PASSWORD_LENGTH = 100
|
2017-06-16 14:40:41 +02:00
|
|
|
full_name = forms.CharField(max_length=UserProfile.MAX_NAME_LENGTH)
|
2014-03-28 00:45:03 +01:00
|
|
|
# The required-ness of the password field gets overridden if it isn't
|
|
|
|
# actually required for a realm
|
2017-08-07 10:12:37 +02:00
|
|
|
password = forms.CharField(widget=forms.PasswordInput, max_length=MAX_PASSWORD_LENGTH)
|
2017-03-23 00:15:06 +01:00
|
|
|
realm_subdomain = forms.CharField(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH, required=False)
|
2016-09-16 19:05:14 +02:00
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
2017-06-15 19:24:38 +02:00
|
|
|
# Since the superclass doesn't except random extra kwargs, we
|
|
|
|
# remove it from the kwargs dict before initializing.
|
2017-10-05 01:45:43 +02:00
|
|
|
self.realm_creation = kwargs['realm_creation']
|
2017-06-15 19:24:38 +02:00
|
|
|
del kwargs['realm_creation']
|
|
|
|
|
2017-10-27 08:28:23 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2017-06-15 11:35:04 +02:00
|
|
|
if settings.TERMS_OF_SERVICE:
|
|
|
|
self.fields['terms'] = forms.BooleanField(required=True)
|
2017-06-15 19:24:46 +02:00
|
|
|
self.fields['realm_name'] = forms.CharField(
|
|
|
|
max_length=Realm.MAX_REALM_NAME_LENGTH,
|
2017-10-05 01:45:43 +02:00
|
|
|
required=self.realm_creation)
|
2012-09-28 22:47:05 +02:00
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def clean_full_name(self) -> str:
|
2017-02-08 05:04:14 +01:00
|
|
|
try:
|
|
|
|
return check_full_name(self.cleaned_data['full_name'])
|
|
|
|
except JsonableError as e:
|
2017-07-20 00:22:36 +02:00
|
|
|
raise ValidationError(e.msg)
|
2017-02-08 05:04:14 +01:00
|
|
|
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
def clean_password(self) -> str:
|
|
|
|
password = self.cleaned_data['password']
|
|
|
|
if self.fields['password'].required and not check_password_strength(password):
|
|
|
|
# The frontend code tries to stop the user from submitting the form with a weak password,
|
|
|
|
# but if the user bypasses that protection, this error code path will run.
|
|
|
|
raise ValidationError(mark_safe(PASSWORD_TOO_WEAK_ERROR))
|
|
|
|
|
|
|
|
return password
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def clean_realm_subdomain(self) -> str:
|
2017-10-19 08:03:40 +02:00
|
|
|
if not self.realm_creation:
|
2017-10-19 08:30:40 +02:00
|
|
|
# This field is only used if realm_creation
|
|
|
|
return ""
|
|
|
|
|
2016-10-31 23:28:20 +01:00
|
|
|
subdomain = self.cleaned_data['realm_subdomain']
|
2017-10-19 08:30:40 +02:00
|
|
|
if 'realm_in_root_domain' in self.data:
|
|
|
|
subdomain = Realm.SUBDOMAIN_FOR_ROOT_DOMAIN
|
|
|
|
|
2017-10-04 02:43:55 +02:00
|
|
|
check_subdomain_available(subdomain)
|
2016-10-31 23:28:20 +01:00
|
|
|
return subdomain
|
2016-07-19 14:35:08 +02:00
|
|
|
|
2013-01-08 23:26:40 +01:00
|
|
|
class ToSForm(forms.Form):
|
|
|
|
terms = forms.BooleanField(required=True)
|
|
|
|
|
2012-09-28 22:47:05 +02:00
|
|
|
class HomepageForm(forms.Form):
|
2017-08-25 07:12:26 +02:00
|
|
|
email = forms.EmailField()
|
2012-12-13 21:08:07 +01:00
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
2016-12-24 02:34:36 +01:00
|
|
|
self.realm = kwargs.pop('realm', None)
|
2017-08-10 22:34:17 +02:00
|
|
|
self.from_multiuse_invite = kwargs.pop('from_multiuse_invite', False)
|
2017-10-27 08:28:23 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2013-08-07 17:59:45 +02:00
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def clean_email(self) -> str:
|
2016-07-27 02:39:14 +02:00
|
|
|
"""Returns the email if and only if the user's email address is
|
|
|
|
allowed to join the realm they are trying to join."""
|
2016-11-05 16:28:55 +01:00
|
|
|
email = self.cleaned_data['email']
|
2016-11-06 00:49:35 +01:00
|
|
|
|
|
|
|
# Otherwise, the user is trying to join a specific realm.
|
2016-12-24 02:34:36 +01:00
|
|
|
realm = self.realm
|
2017-08-10 22:34:17 +02:00
|
|
|
from_multiuse_invite = self.from_multiuse_invite
|
2016-11-06 00:55:39 +01:00
|
|
|
|
2016-11-06 01:44:52 +01:00
|
|
|
if realm is None:
|
2017-10-02 08:32:09 +02:00
|
|
|
raise ValidationError(_("The organization you are trying to "
|
|
|
|
"join using {email} does not "
|
|
|
|
"exist.").format(email=email))
|
2016-11-06 01:44:52 +01:00
|
|
|
|
2017-08-10 22:34:17 +02:00
|
|
|
if not from_multiuse_invite and realm.invite_required:
|
2017-04-14 11:01:24 +02:00
|
|
|
raise ValidationError(_("Please request an invite for {email} "
|
|
|
|
"from the organization "
|
|
|
|
"administrator.").format(email=email))
|
2016-11-06 00:49:35 +01:00
|
|
|
|
2018-03-14 12:54:05 +01:00
|
|
|
try:
|
|
|
|
email_allowed_for_realm(email, realm)
|
|
|
|
except DomainNotAllowedForRealmError:
|
2016-11-06 01:41:38 +01:00
|
|
|
raise ValidationError(
|
2017-04-14 11:01:24 +02:00
|
|
|
_("Your email address, {email}, is not in one of the domains "
|
2017-04-20 20:39:16 +02:00
|
|
|
"that are allowed to register for accounts in this organization.").format(
|
|
|
|
string_id=realm.string_id, email=email))
|
2018-03-14 13:25:26 +01:00
|
|
|
except DisposableEmailError:
|
2018-03-15 22:43:40 +01:00
|
|
|
raise ValidationError(_("Please use your real email address."))
|
2018-06-20 13:08:07 +02:00
|
|
|
except EmailContainsPlusError:
|
|
|
|
raise ValidationError(_("Email addresses containing + are not allowed in this organization."))
|
2016-11-06 01:41:38 +01:00
|
|
|
|
2020-03-02 13:24:50 +01:00
|
|
|
validate_email_not_already_in_realm(realm, email)
|
2017-08-25 07:12:26 +02:00
|
|
|
|
2016-11-06 00:29:55 +01:00
|
|
|
if realm.is_zephyr_mirror_realm:
|
|
|
|
email_is_not_mit_mailing_list(email)
|
2016-07-27 02:39:14 +02:00
|
|
|
|
2016-11-06 00:29:55 +01:00
|
|
|
return email
|
2013-08-07 17:59:45 +02:00
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def email_is_not_disposable(email: str) -> None:
|
2016-11-11 21:13:30 +01:00
|
|
|
if is_disposable_domain(email_to_domain(email)):
|
2016-11-05 03:26:30 +01:00
|
|
|
raise ValidationError(_("Please use your real email address."))
|
|
|
|
|
2016-06-03 01:02:58 +02:00
|
|
|
class RealmCreationForm(forms.Form):
|
2016-12-24 03:24:15 +01:00
|
|
|
# This form determines whether users can create a new realm.
|
2017-11-22 20:05:53 +01:00
|
|
|
email = forms.EmailField(validators=[email_not_system_bot,
|
|
|
|
email_is_not_disposable])
|
2016-06-03 01:02:58 +02:00
|
|
|
|
2012-12-13 21:08:07 +01:00
|
|
|
class LoggingSetPasswordForm(SetPasswordForm):
|
2020-05-28 11:22:45 +02:00
|
|
|
new_password1 = forms.CharField(
|
|
|
|
label=_("New password"),
|
|
|
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
|
|
|
strip=False,
|
|
|
|
help_text=password_validation.password_validators_help_text_html(),
|
|
|
|
max_length=RegistrationForm.MAX_PASSWORD_LENGTH,
|
|
|
|
)
|
|
|
|
new_password2 = forms.CharField(
|
|
|
|
label=_("New password confirmation"),
|
|
|
|
strip=False,
|
|
|
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
|
|
|
max_length=RegistrationForm.MAX_PASSWORD_LENGTH,
|
|
|
|
)
|
|
|
|
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
def clean_new_password1(self) -> str:
|
|
|
|
new_password = self.cleaned_data['new_password1']
|
|
|
|
if not check_password_strength(new_password):
|
|
|
|
# The frontend code tries to stop the user from submitting the form with a weak password,
|
|
|
|
# but if the user bypasses that protection, this error code path will run.
|
|
|
|
raise ValidationError(PASSWORD_TOO_WEAK_ERROR)
|
|
|
|
|
|
|
|
return new_password
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def save(self, commit: bool=True) -> UserProfile:
|
2013-03-29 17:39:53 +01:00
|
|
|
do_change_password(self.user, self.cleaned_data['new_password1'],
|
2017-03-14 06:07:14 +01:00
|
|
|
commit=commit)
|
2012-12-13 21:08:07 +01:00
|
|
|
return self.user
|
2013-05-03 00:26:53 +02:00
|
|
|
|
2018-08-23 21:04:46 +02:00
|
|
|
def generate_password_reset_url(user_profile: UserProfile,
|
|
|
|
token_generator: PasswordResetTokenGenerator) -> str:
|
|
|
|
token = token_generator.make_token(user_profile)
|
2018-02-02 05:43:18 +01:00
|
|
|
uid = urlsafe_base64_encode(force_bytes(user_profile.id))
|
2018-08-23 21:04:46 +02:00
|
|
|
endpoint = reverse('django.contrib.auth.views.password_reset_confirm',
|
|
|
|
kwargs=dict(uidb64=uid, token=token))
|
2020-06-09 00:25:09 +02:00
|
|
|
return f"{user_profile.realm.uri}{endpoint}"
|
2018-08-23 21:04:46 +02:00
|
|
|
|
2016-04-28 23:07:41 +02:00
|
|
|
class ZulipPasswordResetForm(PasswordResetForm):
|
2017-10-04 07:30:17 +02:00
|
|
|
def save(self,
|
2017-12-14 10:31:31 +01:00
|
|
|
domain_override: Optional[bool]=None,
|
2018-05-11 01:39:17 +02:00
|
|
|
subject_template_name: str='registration/password_reset_subject.txt',
|
|
|
|
email_template_name: str='registration/password_reset_email.html',
|
2017-12-14 10:31:31 +01:00
|
|
|
use_https: bool=False,
|
|
|
|
token_generator: PasswordResetTokenGenerator=default_token_generator,
|
2018-05-11 01:39:17 +02:00
|
|
|
from_email: Optional[str]=None,
|
2017-12-14 10:31:31 +01:00
|
|
|
request: HttpRequest=None,
|
2018-05-11 01:39:17 +02:00
|
|
|
html_email_template_name: Optional[str]=None,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
extra_email_context: Optional[Dict[str, Any]]=None,
|
2017-12-14 10:31:31 +01:00
|
|
|
) -> None:
|
2017-10-04 07:30:17 +02:00
|
|
|
"""
|
2017-08-11 07:55:51 +02:00
|
|
|
If the email address has an account in the target realm,
|
|
|
|
generates a one-use only link for resetting password and sends
|
|
|
|
to the user.
|
2017-10-04 07:30:17 +02:00
|
|
|
|
2017-08-11 07:55:51 +02:00
|
|
|
We send a different email if an associated account does not exist in the
|
|
|
|
database, or an account does exist, but not in the realm.
|
2017-10-24 20:33:06 +02:00
|
|
|
|
2017-11-20 19:40:33 +01:00
|
|
|
Note: We ignore protocol and the various email template arguments (those
|
|
|
|
are an artifact of using Django's password reset framework).
|
2017-04-24 12:19:54 +02:00
|
|
|
"""
|
2017-10-04 07:30:17 +02:00
|
|
|
email = self.cleaned_data["email"]
|
2017-08-11 07:55:51 +02:00
|
|
|
|
2017-11-25 03:21:53 +01:00
|
|
|
realm = get_realm(get_subdomain(request))
|
2017-08-11 07:55:51 +02:00
|
|
|
|
|
|
|
if not email_auth_enabled(realm):
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.info("Password reset attempted for %s even though password auth is disabled.", email)
|
2017-08-11 07:55:51 +02:00
|
|
|
return
|
2018-05-29 07:09:48 +02:00
|
|
|
if email_belongs_to_ldap(realm, email):
|
|
|
|
# TODO: Ideally, we'd provide a user-facing error here
|
|
|
|
# about the fact that they aren't allowed to have a
|
|
|
|
# password in the Zulip server and should change it in LDAP.
|
|
|
|
logging.info("Password reset not allowed for user in LDAP domain")
|
|
|
|
return
|
2018-05-21 05:02:27 +02:00
|
|
|
if realm.deactivated:
|
|
|
|
logging.info("Realm is deactivated")
|
|
|
|
return
|
2017-08-11 07:55:51 +02:00
|
|
|
|
2019-12-30 21:13:02 +01:00
|
|
|
if settings.RATE_LIMITING:
|
|
|
|
try:
|
|
|
|
rate_limit_password_reset_form_by_email(email)
|
|
|
|
except RateLimited:
|
|
|
|
# TODO: Show an informative, user-facing error message.
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.info("Too many password reset attempts for email %s", email)
|
2019-12-30 21:13:02 +01:00
|
|
|
return
|
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
user: Optional[UserProfile] = None
|
2017-08-11 07:55:51 +02:00
|
|
|
try:
|
2018-12-07 00:05:57 +01:00
|
|
|
user = get_user_by_delivery_email(email, realm)
|
2017-08-11 07:55:51 +02:00
|
|
|
except UserProfile.DoesNotExist:
|
2017-12-06 05:50:41 +01:00
|
|
|
pass
|
2017-08-11 07:55:51 +02:00
|
|
|
|
|
|
|
context = {
|
|
|
|
'email': email,
|
|
|
|
'realm_uri': realm.uri,
|
2018-12-20 09:44:18 +01:00
|
|
|
'realm_name': realm.name,
|
2017-08-11 07:55:51 +02:00
|
|
|
}
|
|
|
|
|
2018-08-16 18:10:11 +02:00
|
|
|
if user is not None and not user.is_active:
|
|
|
|
context['user_deactivated'] = True
|
|
|
|
user = None
|
|
|
|
|
2017-11-25 03:21:53 +01:00
|
|
|
if user is not None:
|
2018-08-14 22:06:47 +02:00
|
|
|
context['active_account_in_realm'] = True
|
2018-08-23 21:04:46 +02:00
|
|
|
context['reset_url'] = generate_password_reset_url(user, token_generator)
|
2018-12-03 23:26:51 +01:00
|
|
|
send_email('zerver/emails/password_reset', to_user_ids=[user.id],
|
2020-02-14 13:58:58 +01:00
|
|
|
from_name=FromAddress.security_email_from_name(user_profile=user),
|
2018-06-19 14:50:36 +02:00
|
|
|
from_address=FromAddress.tokenized_no_reply_address(),
|
|
|
|
context=context)
|
2017-08-11 07:55:51 +02:00
|
|
|
else:
|
2018-08-14 22:06:47 +02:00
|
|
|
context['active_account_in_realm'] = False
|
2018-12-07 00:05:57 +01:00
|
|
|
active_accounts_in_other_realms = UserProfile.objects.filter(
|
|
|
|
delivery_email__iexact=email, is_active=True)
|
2018-08-18 00:38:28 +02:00
|
|
|
if active_accounts_in_other_realms:
|
|
|
|
context['active_accounts_in_other_realms'] = active_accounts_in_other_realms
|
2020-02-14 13:58:58 +01:00
|
|
|
language = request.LANGUAGE_CODE
|
2018-12-03 23:26:51 +01:00
|
|
|
send_email('zerver/emails/password_reset', to_emails=[email],
|
2020-02-14 13:58:58 +01:00
|
|
|
from_name=FromAddress.security_email_from_name(language=language),
|
2018-06-19 14:50:36 +02:00
|
|
|
from_address=FromAddress.tokenized_no_reply_address(),
|
2020-06-14 13:32:38 +02:00
|
|
|
language=language, context=context,
|
|
|
|
realm=realm)
|
2017-04-24 12:19:54 +02:00
|
|
|
|
2019-12-30 21:13:02 +01:00
|
|
|
class RateLimitedPasswordResetByEmail(RateLimitedObject):
|
|
|
|
def __init__(self, email: str) -> None:
|
|
|
|
self.email = email
|
2020-03-05 13:38:20 +01:00
|
|
|
super().__init__()
|
2019-12-30 21:13:02 +01:00
|
|
|
|
2020-03-06 10:49:04 +01:00
|
|
|
def key(self) -> str:
|
2020-06-09 00:25:09 +02:00
|
|
|
return f"{type(self).__name__}:{self.email}"
|
2019-12-30 21:13:02 +01:00
|
|
|
|
|
|
|
def rules(self) -> List[Tuple[int, int]]:
|
|
|
|
return settings.RATE_LIMITING_RULES['password_reset_form_by_email']
|
|
|
|
|
|
|
|
def rate_limit_password_reset_form_by_email(email: str) -> None:
|
2020-03-04 14:05:25 +01:00
|
|
|
ratelimited, _ = RateLimitedPasswordResetByEmail(email).rate_limit()
|
2019-12-30 21:13:02 +01:00
|
|
|
if ratelimited:
|
|
|
|
raise RateLimited
|
|
|
|
|
2013-12-09 22:26:10 +01:00
|
|
|
class CreateUserForm(forms.Form):
|
2013-05-03 00:26:53 +02:00
|
|
|
full_name = forms.CharField(max_length=100)
|
|
|
|
email = forms.EmailField()
|
2014-01-07 19:51:18 +01:00
|
|
|
|
|
|
|
class OurAuthenticationForm(AuthenticationForm):
|
2017-11-27 07:33:05 +01:00
|
|
|
def clean(self) -> Dict[str, Any]:
|
2017-10-23 20:42:37 +02:00
|
|
|
username = self.cleaned_data.get('username')
|
|
|
|
password = self.cleaned_data.get('password')
|
|
|
|
|
|
|
|
if username is not None and password:
|
|
|
|
subdomain = get_subdomain(self.request)
|
2019-05-04 04:47:44 +02:00
|
|
|
try:
|
2019-05-05 01:04:48 +02:00
|
|
|
realm = get_realm(subdomain)
|
2019-05-04 04:47:44 +02:00
|
|
|
except Realm.DoesNotExist:
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.warning("User %s attempted to password login to nonexistent subdomain %s",
|
|
|
|
username, subdomain)
|
2019-05-05 01:04:48 +02:00
|
|
|
raise ValidationError("Realm does not exist")
|
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
return_data: Dict[str, Any] = {}
|
2019-12-30 02:21:51 +01:00
|
|
|
try:
|
|
|
|
self.user_cache = authenticate(request=self.request, username=username, password=password,
|
|
|
|
realm=realm, return_data=return_data)
|
|
|
|
except RateLimited as e:
|
2020-04-01 13:13:06 +02:00
|
|
|
secs_to_freedom = int(float(str(e)))
|
2019-12-30 02:21:51 +01:00
|
|
|
raise ValidationError(AUTHENTICATION_RATE_LIMITED_ERROR % (secs_to_freedom,))
|
2017-11-18 02:03:36 +01:00
|
|
|
|
2017-11-18 02:23:03 +01:00
|
|
|
if return_data.get("inactive_realm"):
|
|
|
|
raise AssertionError("Programming error: inactive realm in authentication form")
|
2017-11-18 02:03:36 +01:00
|
|
|
|
|
|
|
if return_data.get("inactive_user") and not return_data.get("is_mirror_dummy"):
|
|
|
|
# We exclude mirror dummy accounts here. They should be treated as the
|
|
|
|
# user never having had an account, so we let them fall through to the
|
|
|
|
# normal invalid_login case below.
|
2019-04-12 06:24:58 +02:00
|
|
|
raise ValidationError(mark_safe(DEACTIVATED_ACCOUNT_ERROR))
|
2017-11-18 02:03:36 +01:00
|
|
|
|
|
|
|
if return_data.get("invalid_subdomain"):
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.warning("User %s attempted to password login to wrong subdomain %s",
|
|
|
|
username, subdomain)
|
2017-11-18 02:03:36 +01:00
|
|
|
raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR))
|
|
|
|
|
2017-10-23 20:42:37 +02:00
|
|
|
if self.user_cache is None:
|
|
|
|
raise forms.ValidationError(
|
|
|
|
self.error_messages['invalid_login'],
|
|
|
|
code='invalid_login',
|
|
|
|
params={'username': self.username_field.verbose_name},
|
|
|
|
)
|
2017-11-18 02:03:36 +01:00
|
|
|
|
|
|
|
self.confirm_login_allowed(self.user_cache)
|
2017-10-23 20:42:37 +02:00
|
|
|
|
|
|
|
return self.cleaned_data
|
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def add_prefix(self, field_name: str) -> str:
|
2017-07-12 09:43:39 +02:00
|
|
|
"""Disable prefix, since Zulip doesn't use this Django forms feature
|
|
|
|
(and django-two-factor does use it), and we'd like both to be
|
|
|
|
happy with this form.
|
|
|
|
"""
|
|
|
|
return field_name
|
|
|
|
|
2017-12-20 07:57:26 +01:00
|
|
|
class AuthenticationTokenForm(TwoFactorAuthenticationTokenForm):
|
|
|
|
"""
|
|
|
|
We add this form to update the widget of otp_token. The default
|
|
|
|
widget is an input element whose type is a number, which doesn't
|
|
|
|
stylistically match our theme.
|
|
|
|
"""
|
|
|
|
otp_token = forms.IntegerField(label=_("Token"), min_value=1,
|
|
|
|
max_value=int('9' * totp_digits()),
|
|
|
|
widget=forms.TextInput)
|
|
|
|
|
2016-12-20 10:41:46 +01:00
|
|
|
class MultiEmailField(forms.Field):
|
2018-05-11 01:39:17 +02:00
|
|
|
def to_python(self, emails: str) -> List[str]:
|
2016-12-20 10:41:46 +01:00
|
|
|
"""Normalize data to a list of strings."""
|
|
|
|
if not emails:
|
|
|
|
return []
|
|
|
|
|
|
|
|
return [email.strip() for email in emails.split(',')]
|
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def validate(self, emails: List[str]) -> None:
|
2016-12-20 10:41:46 +01:00
|
|
|
"""Check if value consists only of valid emails."""
|
2017-10-27 08:28:23 +02:00
|
|
|
super().validate(emails)
|
2016-12-20 10:41:46 +01:00
|
|
|
for email in emails:
|
|
|
|
validate_email(email)
|
|
|
|
|
|
|
|
class FindMyTeamForm(forms.Form):
|
|
|
|
emails = MultiEmailField(
|
2017-03-29 07:52:51 +02:00
|
|
|
help_text=_("Add up to 10 comma-separated email addresses."))
|
2016-12-20 10:41:46 +01:00
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def clean_emails(self) -> List[str]:
|
2016-12-20 10:41:46 +01:00
|
|
|
emails = self.cleaned_data['emails']
|
|
|
|
if len(emails) > 10:
|
2017-03-29 07:52:51 +02:00
|
|
|
raise forms.ValidationError(_("Please enter at most 10 emails."))
|
2016-12-20 10:41:46 +01:00
|
|
|
|
|
|
|
return emails
|
2018-08-25 14:06:17 +02:00
|
|
|
|
|
|
|
class RealmRedirectForm(forms.Form):
|
|
|
|
subdomain = forms.CharField(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH, required=True)
|
|
|
|
|
|
|
|
def clean_subdomain(self) -> str:
|
|
|
|
subdomain = self.cleaned_data['subdomain']
|
2019-05-04 04:47:44 +02:00
|
|
|
try:
|
|
|
|
get_realm(subdomain)
|
|
|
|
except Realm.DoesNotExist:
|
2018-08-25 14:06:17 +02:00
|
|
|
raise ValidationError(_("We couldn't find that Zulip organization."))
|
|
|
|
return subdomain
|