mirror of https://github.com/zulip/zulip.git
518 lines
24 KiB
Python
518 lines
24 KiB
Python
# -*- coding: utf-8 -*-
|
|
from typing import Any, List, Dict, Mapping, Optional
|
|
|
|
from django.utils.translation import ugettext as _
|
|
from django.conf import settings
|
|
from django.contrib.auth import authenticate, get_backends
|
|
from django.urls import reverse
|
|
from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse, HttpRequest
|
|
from django.shortcuts import redirect, render
|
|
from django.template import RequestContext, loader
|
|
from django.utils.timezone import now
|
|
from django.core.exceptions import ValidationError
|
|
from django.core import validators
|
|
from zerver.context_processors import get_realm_from_request
|
|
from zerver.models import UserProfile, Realm, Stream, MultiuseInvite, \
|
|
name_changes_disabled, email_to_username, email_allowed_for_realm, \
|
|
get_realm, get_user, get_default_stream_groups, DisposableEmailError, \
|
|
DomainNotAllowedForRealmError, get_source_profile, EmailContainsPlusError
|
|
from zerver.lib.send_email import send_email, FromAddress
|
|
from zerver.lib.events import do_events_register
|
|
from zerver.lib.actions import do_change_password, do_change_full_name, do_change_is_admin, \
|
|
do_activate_user, do_create_user, do_create_realm, \
|
|
email_not_system_bot, validate_email_for_realm, \
|
|
do_set_user_display_setting, lookup_default_stream_groups, bulk_add_subscriptions
|
|
from zerver.forms import RegistrationForm, HomepageForm, RealmCreationForm, \
|
|
CreateUserForm, FindMyTeamForm
|
|
from django_auth_ldap.backend import LDAPBackend, _LDAPUser
|
|
from zerver.decorator import require_post, has_request_variables, \
|
|
JsonableError, REQ, do_login
|
|
from zerver.lib.onboarding import setup_initial_streams, \
|
|
send_initial_realm_messages, setup_realm_internal_bots
|
|
from zerver.lib.response import json_success
|
|
from zerver.lib.subdomains import get_subdomain, is_root_domain_available
|
|
from zerver.lib.timezone import get_all_timezones
|
|
from zerver.lib.users import get_accounts_for_email
|
|
from zerver.lib.zephyr import compute_mit_user_fullname
|
|
from zerver.views.auth import create_preregistration_user, \
|
|
redirect_and_log_into_subdomain, \
|
|
redirect_to_deactivation_notice
|
|
from zproject.backends import ldap_auth_enabled, password_auth_enabled, ZulipLDAPAuthBackend, \
|
|
ZulipLDAPException, email_auth_enabled
|
|
|
|
from confirmation.models import Confirmation, RealmCreationKey, ConfirmationKeyException, \
|
|
validate_key, create_confirmation_link, get_object_from_key, \
|
|
render_confirmation_key_error
|
|
|
|
import logging
|
|
import requests
|
|
import smtplib
|
|
import ujson
|
|
|
|
import urllib
|
|
|
|
def check_prereg_key_and_redirect(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
|
# If the key isn't valid, show the error message on the original URL
|
|
confirmation = Confirmation.objects.filter(confirmation_key=confirmation_key).first()
|
|
if confirmation is None or confirmation.type not in [
|
|
Confirmation.USER_REGISTRATION, Confirmation.INVITATION, Confirmation.REALM_CREATION]:
|
|
return render_confirmation_key_error(
|
|
request, ConfirmationKeyException(ConfirmationKeyException.DOES_NOT_EXIST))
|
|
try:
|
|
get_object_from_key(confirmation_key, confirmation.type)
|
|
except ConfirmationKeyException as exception:
|
|
return render_confirmation_key_error(request, exception)
|
|
|
|
# confirm_preregistrationuser.html just extracts the confirmation_key
|
|
# (and GET parameters) and redirects to /accounts/register, so that the
|
|
# user can enter their information on a cleaner URL.
|
|
return render(request, 'confirmation/confirm_preregistrationuser.html',
|
|
context={
|
|
'key': confirmation_key,
|
|
'full_name': request.GET.get("full_name", None)})
|
|
|
|
@require_post
|
|
def accounts_register(request: HttpRequest) -> HttpResponse:
|
|
key = request.POST['key']
|
|
confirmation = Confirmation.objects.get(confirmation_key=key)
|
|
prereg_user = confirmation.content_object
|
|
email = prereg_user.email
|
|
realm_creation = prereg_user.realm_creation
|
|
password_required = prereg_user.password_required
|
|
is_realm_admin = prereg_user.invited_as_admin or realm_creation
|
|
|
|
try:
|
|
validators.validate_email(email)
|
|
except ValidationError:
|
|
return render(request, "zerver/invalid_email.html", context={"invalid_email": True})
|
|
|
|
if realm_creation:
|
|
# For creating a new realm, there is no existing realm or domain
|
|
realm = None
|
|
else:
|
|
realm = get_realm(get_subdomain(request))
|
|
if realm is None or realm != prereg_user.realm:
|
|
return render_confirmation_key_error(
|
|
request, ConfirmationKeyException(ConfirmationKeyException.DOES_NOT_EXIST))
|
|
|
|
try:
|
|
email_allowed_for_realm(email, realm)
|
|
except DomainNotAllowedForRealmError:
|
|
return render(request, "zerver/invalid_email.html",
|
|
context={"realm_name": realm.name, "closed_domain": True})
|
|
except DisposableEmailError:
|
|
return render(request, "zerver/invalid_email.html",
|
|
context={"realm_name": realm.name, "disposable_emails_not_allowed": True})
|
|
except EmailContainsPlusError:
|
|
return render(request, "zerver/invalid_email.html",
|
|
context={"realm_name": realm.name, "email_contains_plus": True})
|
|
|
|
if realm.deactivated:
|
|
# The user is trying to register for a deactivated realm. Advise them to
|
|
# contact support.
|
|
return redirect_to_deactivation_notice()
|
|
|
|
try:
|
|
validate_email_for_realm(realm, email)
|
|
except ValidationError: # nocoverage # We need to add a test for this.
|
|
return HttpResponseRedirect(reverse('django.contrib.auth.views.login') + '?email=' +
|
|
urllib.parse.quote_plus(email))
|
|
|
|
name_validated = False
|
|
full_name = None
|
|
|
|
if request.POST.get('from_confirmation'):
|
|
try:
|
|
del request.session['authenticated_full_name']
|
|
except KeyError:
|
|
pass
|
|
if realm is not None and realm.is_zephyr_mirror_realm:
|
|
# For MIT users, we can get an authoritative name from Hesiod.
|
|
# Technically we should check that this is actually an MIT
|
|
# realm, but we can cross that bridge if we ever get a non-MIT
|
|
# zephyr mirroring realm.
|
|
hesiod_name = compute_mit_user_fullname(email)
|
|
form = RegistrationForm(
|
|
initial={'full_name': hesiod_name if "@" not in hesiod_name else ""},
|
|
realm_creation=realm_creation)
|
|
name_validated = True
|
|
elif settings.POPULATE_PROFILE_VIA_LDAP:
|
|
for backend in get_backends():
|
|
if isinstance(backend, LDAPBackend):
|
|
try:
|
|
ldap_username = backend.django_to_ldap_username(email)
|
|
except ZulipLDAPException:
|
|
logging.warning("New account email %s could not be found in LDAP" % (email,))
|
|
form = RegistrationForm(realm_creation=realm_creation)
|
|
break
|
|
|
|
ldap_attrs = _LDAPUser(backend, ldap_username).attrs
|
|
|
|
try:
|
|
ldap_full_name = ldap_attrs[settings.AUTH_LDAP_USER_ATTR_MAP['full_name']][0]
|
|
request.session['authenticated_full_name'] = ldap_full_name
|
|
name_validated = True
|
|
# We don't use initial= here, because if the form is
|
|
# complete (that is, no additional fields need to be
|
|
# filled out by the user) we want the form to validate,
|
|
# so they can be directly registered without having to
|
|
# go through this interstitial.
|
|
form = RegistrationForm({'full_name': ldap_full_name},
|
|
realm_creation=realm_creation)
|
|
# FIXME: This will result in the user getting
|
|
# validation errors if they have to enter a password.
|
|
# Not relevant for ONLY_SSO, though.
|
|
break
|
|
except TypeError:
|
|
# Let the user fill out a name and/or try another backend
|
|
form = RegistrationForm(realm_creation=realm_creation)
|
|
elif 'full_name' in request.POST:
|
|
form = RegistrationForm(
|
|
initial={'full_name': request.POST.get('full_name')},
|
|
realm_creation=realm_creation
|
|
)
|
|
else:
|
|
form = RegistrationForm(realm_creation=realm_creation)
|
|
else:
|
|
postdata = request.POST.copy()
|
|
if name_changes_disabled(realm):
|
|
# If we populate profile information via LDAP and we have a
|
|
# verified name from you on file, use that. Otherwise, fall
|
|
# back to the full name in the request.
|
|
try:
|
|
postdata.update({'full_name': request.session['authenticated_full_name']})
|
|
name_validated = True
|
|
except KeyError:
|
|
pass
|
|
form = RegistrationForm(postdata, realm_creation=realm_creation)
|
|
if not (password_auth_enabled(realm) and password_required):
|
|
form['password'].field.required = False
|
|
|
|
if form.is_valid():
|
|
if password_auth_enabled(realm):
|
|
password = form.cleaned_data['password']
|
|
else:
|
|
# SSO users don't need no passwords
|
|
password = None
|
|
|
|
if realm_creation:
|
|
string_id = form.cleaned_data['realm_subdomain']
|
|
realm_name = form.cleaned_data['realm_name']
|
|
realm = do_create_realm(string_id, realm_name)
|
|
setup_initial_streams(realm)
|
|
setup_realm_internal_bots(realm)
|
|
assert(realm is not None)
|
|
|
|
full_name = form.cleaned_data['full_name']
|
|
short_name = email_to_username(email)
|
|
default_stream_group_names = request.POST.getlist('default_stream_group')
|
|
default_stream_groups = lookup_default_stream_groups(default_stream_group_names, realm)
|
|
|
|
timezone = ""
|
|
if 'timezone' in request.POST and request.POST['timezone'] in get_all_timezones():
|
|
timezone = request.POST['timezone']
|
|
|
|
if 'source_realm' in request.POST and request.POST["source_realm"] != "on":
|
|
source_profile = get_source_profile(email, request.POST["source_realm"])
|
|
else:
|
|
source_profile = None
|
|
|
|
if not realm_creation:
|
|
try:
|
|
existing_user_profile = get_user(email, realm) # type: Optional[UserProfile]
|
|
except UserProfile.DoesNotExist:
|
|
existing_user_profile = None
|
|
else:
|
|
existing_user_profile = None
|
|
|
|
return_data = {} # type: Dict[str, bool]
|
|
if ldap_auth_enabled(realm):
|
|
# If the user was authenticated using an external SSO
|
|
# mechanism like Google or GitHub auth, then authentication
|
|
# will have already been done before creating the
|
|
# PreregistrationUser object with password_required=False, and
|
|
# so we don't need to worry about passwords.
|
|
#
|
|
# If instead the realm is using EmailAuthBackend, we will
|
|
# set their password above.
|
|
#
|
|
# But if the realm is using LDAPAuthBackend, we need to verify
|
|
# their LDAP password (which will, as a side effect, create
|
|
# the user account) here using authenticate.
|
|
auth_result = authenticate(request,
|
|
username=email,
|
|
password=password,
|
|
realm=realm,
|
|
return_data=return_data)
|
|
if auth_result is not None:
|
|
# Since we'll have created a user, we now just log them in.
|
|
return login_and_go_to_home(request, auth_result)
|
|
|
|
if return_data.get("outside_ldap_domain") and email_auth_enabled(realm):
|
|
# If both the LDAP and Email auth backends are
|
|
# enabled, and the user's email is outside the LDAP
|
|
# domain, then the intent is to create a user in the
|
|
# realm with their email outside the LDAP organization
|
|
# (with e.g. a password stored in the Zulip database,
|
|
# not LDAP). So we fall through and create the new
|
|
# account.
|
|
#
|
|
# It's likely that we can extend this block to the
|
|
# Google and GitHub auth backends with no code changes
|
|
# other than here.
|
|
pass
|
|
else:
|
|
# TODO: This probably isn't going to give a
|
|
# user-friendly error message, but it doesn't
|
|
# particularly matter, because the registration form
|
|
# is hidden for most users.
|
|
return HttpResponseRedirect(reverse('django.contrib.auth.views.login') + '?email=' +
|
|
urllib.parse.quote_plus(email))
|
|
|
|
if existing_user_profile is not None and existing_user_profile.is_mirror_dummy:
|
|
user_profile = existing_user_profile
|
|
do_activate_user(user_profile)
|
|
do_change_password(user_profile, password)
|
|
do_change_full_name(user_profile, full_name, user_profile)
|
|
do_set_user_display_setting(user_profile, 'timezone', timezone)
|
|
# TODO: When we clean up the `do_activate_user` code path,
|
|
# make it respect invited_as_admin / is_realm_admin.
|
|
else:
|
|
user_profile = do_create_user(email, password, realm, full_name, short_name,
|
|
prereg_user=prereg_user, is_realm_admin=is_realm_admin,
|
|
tos_version=settings.TOS_VERSION,
|
|
timezone=timezone,
|
|
newsletter_data={"IP": request.META['REMOTE_ADDR']},
|
|
default_stream_groups=default_stream_groups,
|
|
source_profile=source_profile,
|
|
realm_creation=realm_creation)
|
|
|
|
if realm_creation:
|
|
bulk_add_subscriptions([realm.signup_notifications_stream], [user_profile])
|
|
send_initial_realm_messages(realm)
|
|
|
|
# Because for realm creation, registration happens on the
|
|
# root domain, we need to log them into the subdomain for
|
|
# their new realm.
|
|
return redirect_and_log_into_subdomain(realm, full_name, email)
|
|
|
|
# This dummy_backend check below confirms the user is
|
|
# authenticating to the correct subdomain.
|
|
auth_result = authenticate(username=user_profile.email,
|
|
realm=realm,
|
|
return_data=return_data,
|
|
use_dummy_backend=True)
|
|
if return_data.get('invalid_subdomain'):
|
|
# By construction, this should never happen.
|
|
logging.error("Subdomain mismatch in registration %s: %s" % (
|
|
realm.subdomain, user_profile.email,))
|
|
return redirect('/')
|
|
|
|
return login_and_go_to_home(request, auth_result)
|
|
|
|
return render(
|
|
request,
|
|
'zerver/register.html',
|
|
context={'form': form,
|
|
'email': email,
|
|
'key': key,
|
|
'full_name': request.session.get('authenticated_full_name', None),
|
|
'lock_name': name_validated and name_changes_disabled(realm),
|
|
# password_auth_enabled is normally set via our context processor,
|
|
# but for the registration form, there is no logged in user yet, so
|
|
# we have to set it here.
|
|
'creating_new_team': realm_creation,
|
|
'password_required': password_auth_enabled(realm) and password_required,
|
|
'password_auth_enabled': password_auth_enabled(realm),
|
|
'root_domain_available': is_root_domain_available(),
|
|
'default_stream_groups': get_default_stream_groups(realm),
|
|
'accounts': get_accounts_for_email(email),
|
|
'MAX_REALM_NAME_LENGTH': str(Realm.MAX_REALM_NAME_LENGTH),
|
|
'MAX_NAME_LENGTH': str(UserProfile.MAX_NAME_LENGTH),
|
|
'MAX_PASSWORD_LENGTH': str(form.MAX_PASSWORD_LENGTH),
|
|
'MAX_REALM_SUBDOMAIN_LENGTH': str(Realm.MAX_REALM_SUBDOMAIN_LENGTH)
|
|
}
|
|
)
|
|
|
|
def login_and_go_to_home(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
|
do_login(request, user_profile)
|
|
return HttpResponseRedirect(user_profile.realm.uri + reverse('zerver.views.home.home'))
|
|
|
|
def prepare_activation_url(email: str, request: HttpRequest,
|
|
realm_creation: bool=False,
|
|
streams: Optional[List[Stream]]=None) -> str:
|
|
"""
|
|
Send an email with a confirmation link to the provided e-mail so the user
|
|
can complete their registration.
|
|
"""
|
|
prereg_user = create_preregistration_user(email, request, realm_creation)
|
|
|
|
if streams is not None:
|
|
prereg_user.streams.set(streams)
|
|
|
|
confirmation_type = Confirmation.USER_REGISTRATION
|
|
if realm_creation:
|
|
confirmation_type = Confirmation.REALM_CREATION
|
|
|
|
activation_url = create_confirmation_link(prereg_user, request.get_host(), confirmation_type)
|
|
if settings.DEVELOPMENT and realm_creation:
|
|
request.session['confirmation_key'] = {'confirmation_key': activation_url.split('/')[-1]}
|
|
return activation_url
|
|
|
|
def send_confirm_registration_email(email: str, activation_url: str) -> None:
|
|
send_email('zerver/emails/confirm_registration', to_email=email,
|
|
from_address=FromAddress.tokenized_no_reply_address(),
|
|
context={'activate_url': activation_url})
|
|
|
|
def redirect_to_email_login_url(email: str) -> HttpResponseRedirect:
|
|
login_url = reverse('django.contrib.auth.views.login')
|
|
email = urllib.parse.quote_plus(email)
|
|
redirect_url = login_url + '?already_registered=' + email
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
def create_realm(request: HttpRequest, creation_key: Optional[str]=None) -> HttpResponse:
|
|
try:
|
|
key_record = validate_key(creation_key)
|
|
except RealmCreationKey.Invalid:
|
|
return render(request, "zerver/realm_creation_failed.html",
|
|
context={'message': _('The organization creation link has expired'
|
|
' or is not valid.')})
|
|
if not settings.OPEN_REALM_CREATION:
|
|
if key_record is None:
|
|
return render(request, "zerver/realm_creation_failed.html",
|
|
context={'message': _('New organization creation disabled')})
|
|
|
|
# When settings.OPEN_REALM_CREATION is enabled, anyone can create a new realm,
|
|
# with a few restrictions on their email address.
|
|
if request.method == 'POST':
|
|
form = RealmCreationForm(request.POST)
|
|
if form.is_valid():
|
|
email = form.cleaned_data['email']
|
|
activation_url = prepare_activation_url(email, request, realm_creation=True)
|
|
if key_record is not None and key_record.presume_email_valid:
|
|
# The user has a token created from the server command line;
|
|
# skip confirming the email is theirs, taking their word for it.
|
|
# This is essential on first install if the admin hasn't stopped
|
|
# to configure outbound email up front, or it isn't working yet.
|
|
key_record.delete()
|
|
return HttpResponseRedirect(activation_url)
|
|
|
|
try:
|
|
send_confirm_registration_email(email, activation_url)
|
|
except smtplib.SMTPException as e:
|
|
logging.error('Error in create_realm: %s' % (str(e),))
|
|
return HttpResponseRedirect("/config-error/smtp")
|
|
|
|
if key_record is not None:
|
|
key_record.delete()
|
|
return HttpResponseRedirect(reverse('new_realm_send_confirm', kwargs={'email': email}))
|
|
else:
|
|
form = RealmCreationForm()
|
|
return render(request,
|
|
'zerver/create_realm.html',
|
|
context={'form': form, 'current_url': request.get_full_path},
|
|
)
|
|
|
|
# This is used only by the casper test in 00-realm-creation.js.
|
|
def confirmation_key(request: HttpRequest) -> HttpResponse:
|
|
return json_success(request.session.get('confirmation_key'))
|
|
|
|
def accounts_home(request: HttpRequest, multiuse_object: Optional[MultiuseInvite]=None) -> HttpResponse:
|
|
realm = get_realm(get_subdomain(request))
|
|
|
|
if realm is None:
|
|
return HttpResponseRedirect(reverse('zerver.views.registration.find_account'))
|
|
if realm.deactivated:
|
|
return redirect_to_deactivation_notice()
|
|
|
|
from_multiuse_invite = False
|
|
streams_to_subscribe = None
|
|
|
|
if multiuse_object:
|
|
realm = multiuse_object.realm
|
|
streams_to_subscribe = multiuse_object.streams.all()
|
|
from_multiuse_invite = True
|
|
|
|
if request.method == 'POST':
|
|
form = HomepageForm(request.POST, realm=realm, from_multiuse_invite=from_multiuse_invite)
|
|
if form.is_valid():
|
|
email = form.cleaned_data['email']
|
|
activation_url = prepare_activation_url(email, request, streams=streams_to_subscribe)
|
|
try:
|
|
send_confirm_registration_email(email, activation_url)
|
|
except smtplib.SMTPException as e:
|
|
logging.error('Error in accounts_home: %s' % (str(e),))
|
|
return HttpResponseRedirect("/config-error/smtp")
|
|
|
|
return HttpResponseRedirect(reverse('signup_send_confirm', kwargs={'email': email}))
|
|
|
|
email = request.POST['email']
|
|
try:
|
|
validate_email_for_realm(realm, email)
|
|
except ValidationError:
|
|
return redirect_to_email_login_url(email)
|
|
else:
|
|
form = HomepageForm(realm=realm)
|
|
return render(request,
|
|
'zerver/accounts_home.html',
|
|
context={'form': form, 'current_url': request.get_full_path,
|
|
'from_multiuse_invite': from_multiuse_invite},
|
|
)
|
|
|
|
def accounts_home_from_multiuse_invite(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
|
multiuse_object = None
|
|
try:
|
|
multiuse_object = get_object_from_key(confirmation_key, Confirmation.MULTIUSE_INVITE)
|
|
# Required for oAuth2
|
|
request.session["multiuse_object_key"] = confirmation_key
|
|
except ConfirmationKeyException as exception:
|
|
realm = get_realm_from_request(request)
|
|
if realm is None or realm.invite_required:
|
|
return render_confirmation_key_error(request, exception)
|
|
return accounts_home(request, multiuse_object=multiuse_object)
|
|
|
|
def generate_204(request: HttpRequest) -> HttpResponse:
|
|
return HttpResponse(content=None, status=204)
|
|
|
|
def find_account(request: HttpRequest) -> HttpResponse:
|
|
url = reverse('zerver.views.registration.find_account')
|
|
|
|
emails = [] # type: List[str]
|
|
if request.method == 'POST':
|
|
form = FindMyTeamForm(request.POST)
|
|
if form.is_valid():
|
|
emails = form.cleaned_data['emails']
|
|
for user_profile in UserProfile.objects.filter(
|
|
email__in=emails, is_active=True, is_bot=False, realm__deactivated=False):
|
|
ctx = {
|
|
'full_name': user_profile.full_name,
|
|
'email': user_profile.email,
|
|
'realm_uri': user_profile.realm.uri,
|
|
'realm_name': user_profile.realm.name,
|
|
}
|
|
send_email('zerver/emails/find_team', to_user_id=user_profile.id, context=ctx)
|
|
|
|
# Note: Show all the emails in the result otherwise this
|
|
# feature can be used to ascertain which email addresses
|
|
# are associated with Zulip.
|
|
data = urllib.parse.urlencode({'emails': ','.join(emails)})
|
|
return redirect(url + "?" + data)
|
|
else:
|
|
form = FindMyTeamForm()
|
|
result = request.GET.get('emails')
|
|
# The below validation is perhaps unnecessary, in that we
|
|
# shouldn't get able to get here with an invalid email unless
|
|
# the user hand-edits the URLs.
|
|
if result:
|
|
for email in result.split(','):
|
|
try:
|
|
validators.validate_email(email)
|
|
emails.append(email)
|
|
except ValidationError:
|
|
pass
|
|
|
|
return render(request,
|
|
'zerver/find_account.html',
|
|
context={'form': form, 'current_url': lambda: url,
|
|
'emails': emails},)
|