Files
zulip/zerver/forms.py
Rishi Gupta 777fcaa6a0 Add new organization type field to Realm objects.
Adds a new field org_type to Realm.  Defaults for restricted_to_domain
and invite_required are now controlled by org_type at time of realm
creation (see zerver.lib.actions.do_create_realm), rather than at the
database level.  Note that the backend defaults are all
org_type=corporate, since that matches the current assumptions in the
codebase, whereas the frontend default is org_type=community, since if
a user isn't sure they probably want community.

Since we will likely in the future enable/disable various
administrative features based on whether an organization is corporate
or community, we discuss those issues in the realm creation form.
Before we actually implement any such features, we'll want to make
sure users understand what type of organization they are a member of.

Choice of org_type (via radio button) has been added to the realm
creation flow and the realm creation management command, and the
open-realm option removed.

The database defaults have not been changed, which allows our testing code
to work unchanged.

[includes some HTML/CSS work by Brock Whittaker to make it look nice]
2016-10-05 17:01:46 -07:00

230 lines
9.5 KiB
Python

from __future__ import absolute_import
from typing import Any, Callable, Optional
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import SetPasswordForm, AuthenticationForm, \
PasswordResetForm
from django.conf import settings
from django.db.models.query import QuerySet
from jinja2 import Markup as mark_safe
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from zerver.models import resolve_subdomain_to_realm
from zerver.lib.utils import get_subdomain, check_subdomain
import logging
from zerver.models import Realm, get_user_profile_by_email, UserProfile, \
completely_open, resolve_email_to_domain, get_realm, \
get_unique_open_realm, split_email_to_domain
from zerver.lib.actions import do_change_password, is_inactive, user_email_is_unique
from zproject.backends import password_auth_enabled
import DNS
from six import text_type
SIGNUP_STRING = u'Your e-mail does not match any existing open organization. ' + \
u'Use a different e-mail address, or contact %s with questions.' % (settings.ZULIP_ADMINISTRATOR,)
def subdomain_unavailable(subdomain):
# type: (text_type) -> text_type
return u"The subdomain '%s' is not available. Please choose another one." % (subdomain)
if settings.SHOW_OSS_ANNOUNCEMENT:
SIGNUP_STRING = u'Your e-mail does not match any existing organization. <br />' + \
u"The zulip.com service is not taking new customer teams. <br /> " + \
u"<a href=\"https://blogs.dropbox.com/tech/2015/09/open-sourcing-zulip-a-dropbox-hack-week-project/\">" + \
u"Zulip is open source</a>, so you can install your own Zulip server " + \
u"by following the instructions on <a href=\"https://www.zulip.org\">www.zulip.org</a>!"
MIT_VALIDATION_ERROR = u'That user does not exist at MIT or is a ' + \
u'<a href="https://ist.mit.edu/email-lists">mailing list</a>. ' + \
u'If you want to sign up an alias for Zulip, ' + \
u'<a href="mailto:support@zulipchat.com">contact us</a>.'
WRONG_SUBDOMAIN_ERROR = "Your Zulip account is not a member of the " + \
"organization associated with this subdomain. " + \
"Please contact %s with any questions!" % (settings.ZULIP_ADMINISTRATOR,)
def get_registration_string(domain):
# type: (text_type) -> text_type
register_url = reverse('register') + domain
register_account_string = _('The organization with the domain already exists. '
'Please register your account <a href=%(url)s>here</a>.') % {'url': register_url}
return register_account_string
def get_valid_realm(value):
# type: (str) -> Optional[Realm]
"""Checks if there is a realm without invite_required
matching the domain of the input e-mail."""
realm = get_realm(resolve_email_to_domain(value))
if realm is None or realm.invite_required:
return None
return realm
def not_mit_mailing_list(value):
# type: (str) -> bool
"""Prevent MIT mailing lists from signing up for Zulip"""
if "@mit.edu" in value:
username = value.rsplit("@", 1)[0]
# Check whether the user exists and can get mail.
try:
DNS.dnslookup("%s.pobox.ns.athena.mit.edu" % username, DNS.Type.TXT)
return True
except DNS.Base.ServerError as e:
if e.rcode == DNS.Status.NXDOMAIN:
raise ValidationError(mark_safe(MIT_VALIDATION_ERROR))
else:
raise
return True
class RegistrationForm(forms.Form):
full_name = forms.CharField(max_length=100)
# The required-ness of the password field gets overridden if it isn't
# actually required for a realm
password = forms.CharField(widget=forms.PasswordInput, max_length=100,
required=False)
realm_name = forms.CharField(max_length=100, required=False)
realm_subdomain = forms.CharField(max_length=40, required=False)
realm_org_type = forms.ChoiceField(((Realm.COMMUNITY, 'Community'),
(Realm.CORPORATE, 'Corporate')), \
initial=Realm.COMMUNITY, required=False)
if settings.TERMS_OF_SERVICE:
terms = forms.BooleanField(required=True)
def clean_realm_subdomain(self):
# type: () -> str
data = self.cleaned_data['realm_subdomain']
realm = resolve_subdomain_to_realm(data)
if realm is not None:
raise ValidationError(subdomain_unavailable(data))
return data
class ToSForm(forms.Form):
terms = forms.BooleanField(required=True)
class HomepageForm(forms.Form):
# This form is important because it determines whether users can
# register for our product. Be careful when modifying the
# validators.
email = forms.EmailField(validators=[is_inactive,])
def __init__(self, *args, **kwargs):
# type: (*Any, **Any) -> None
self.domain = kwargs.get("domain")
self.subdomain = kwargs.get("subdomain")
if "domain" in kwargs:
del kwargs["domain"]
if "subdomain" in kwargs:
del kwargs["subdomain"]
super(HomepageForm, self).__init__(*args, **kwargs)
def clean_email(self):
# type: () -> str
"""Returns the email if and only if the user's email address is
allowed to join the realm they are trying to join."""
data = self.cleaned_data['email']
# If the server has a unique open realm, pass
if get_unique_open_realm():
return data
# If a realm is specified and that realm is open, pass
if completely_open(self.domain):
return data
# If the subdomain encodes a complete open realm, pass
subdomain_realm = resolve_subdomain_to_realm(self.subdomain)
if (subdomain_realm is not None and
completely_open(subdomain_realm.domain)):
return data
# If no realm is specified, fail
realm = get_valid_realm(data)
if realm is None:
raise ValidationError(mark_safe(SIGNUP_STRING))
# If it's a clear realm not used for Zephyr mirroring, pass
if not realm.is_zephyr_mirror_realm:
return data
# At this point, the user is trying to join a Zephyr mirroring
# realm. We confirm that they are a real account (not a
# mailing list), and if so, let them in.
if not_mit_mailing_list(data):
return data
# Otherwise, the user is an MIT mailing list, and we return failure
raise ValidationError(mark_safe(SIGNUP_STRING))
class RealmCreationForm(forms.Form):
# This form determines whether users can
# create a new realm. Be careful when modifying the
# validators.
email = forms.EmailField(validators=[user_email_is_unique,])
def __init__(self, *args, **kwargs):
# type: (*Any, **Any) -> None
self.domain = kwargs.get("domain")
if "domain" in kwargs:
del kwargs["domain"]
super(RealmCreationForm, self).__init__(*args, **kwargs)
def clean_email(self):
# type: () -> text_type
data = self.cleaned_data['email']
domain = split_email_to_domain(data)
if (get_realm(domain) is not None):
raise ValidationError(mark_safe(get_registration_string(domain)))
return data
class LoggingSetPasswordForm(SetPasswordForm):
def save(self, commit=True):
# type: (bool) -> UserProfile
do_change_password(self.user, self.cleaned_data['new_password1'],
log=True, commit=commit)
return self.user
class ZulipPasswordResetForm(PasswordResetForm):
def get_users(self, email):
# type: (str) -> QuerySet
"""Given an email, return matching user(s) who should receive a reset.
This is modified from the original in that it allows non-bot
users who don't have a usable password to reset their
passwords.
"""
if not password_auth_enabled:
logging.info("Password reset attempted for %s even though password auth is disabled." % (email,))
return []
result = UserProfile.objects.filter(email__iexact=email, is_active=True,
is_bot=False)
if len(result) == 0:
logging.info("Password reset attempted for %s; no active account." % (email,))
return result
class CreateUserForm(forms.Form):
full_name = forms.CharField(max_length=100)
email = forms.EmailField()
class OurAuthenticationForm(AuthenticationForm):
def clean_username(self):
# type: () -> str
email = self.cleaned_data['username']
try:
user_profile = get_user_profile_by_email(email)
except UserProfile.DoesNotExist:
return email
if user_profile.realm.deactivated:
error_msg = u"""Sorry for the trouble, but %s has been deactivated.
Please contact %s to reactivate this group.""" % (
user_profile.realm.name,
settings.ZULIP_ADMINISTRATOR)
raise ValidationError(mark_safe(error_msg))
if not check_subdomain(get_subdomain(self.request), user_profile.realm.subdomain):
logging.warning("User %s attempted to password login to wrong subdomain %s" %
(user_profile.email, get_subdomain(self.request)))
raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR))
return email