emails: Add backend for disallowing disposable email addresses.

This commit is contained in:
Vishnu Ks 2018-03-06 00:49:07 +05:30 committed by Tim Abbott
parent 41f8618c04
commit a44255eedb
12 changed files with 128 additions and 22 deletions

View File

@ -1,10 +0,0 @@
{% extends "zerver/portico.html" %}
{% block portico_content %}
<h3>{{ _('Private organization') }}</h3>
<p>{{ _('Hi there! Thank you for your interest in Zulip.') }}</p>
<p>{% trans %}The organization you are trying to join, {{ closed_domain_name }}, only allows users with e-mail addresses within the organization. Please ask for a new invite to an appropriate e-mail address.{% endtrans %}</p>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "zerver/portico.html" %}
{% block portico_content %}
<h3>{{ _('Invalid email') }}</h3>
<p>{{ _('Hi there! Thank you for your interest in Zulip.') }}</p>
{% if closed_domain %}
<p>
{% trans %}
The organization you are trying to join, {{ realm_name }},
only allows users with email addresses within the
organization. Please sign up using appropriate email address.
{% endtrans %}
</p>
{% endif %}
{% if disposable_emails_not_allowed %}
<p>
{% trans %}The organization you are trying to join,
{{realm_name}}, does not allow signups using disposable email
addresses. Please sign up using a real email address.
{% endtrans %}
</p>
{% endif %}
{% endblock %}

View File

@ -24,8 +24,8 @@ from zerver.lib.request import JsonableError
from zerver.lib.send_email import send_email, FromAddress
from zerver.lib.subdomains import get_subdomain, user_matches_subdomain, is_root_domain_available
from zerver.lib.users import check_full_name
from zerver.models import Realm, get_user, UserProfile, \
get_realm, email_to_domain, email_allowed_for_realm
from zerver.models import Realm, get_user, UserProfile, get_realm, email_to_domain, \
email_allowed_for_realm, disposable_email_check
from zproject.backends import email_auth_enabled
import logging
@ -151,6 +151,7 @@ class HomepageForm(forms.Form):
"that are allowed to register for accounts in this organization.").format(
string_id=realm.string_id, email=email))
disposable_email_check(realm, email)
validate_email_for_realm(realm, email)
if realm.is_zephyr_mirror_realm:

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-05 19:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('zerver', '0146_userprofile_message_content_in_email_notifications'),
]
operations = [
migrations.AddField(
model_name='realm',
name='disallow_disposable_email_addresses',
field=models.BooleanField(default=True),
),
]

View File

@ -33,6 +33,8 @@ from django.utils.translation import ugettext_lazy as _
from zerver.lib import cache
from zerver.lib.validator import check_int, check_float, check_string, \
check_short_string
from zerver.lib.name_restrictions import is_disposable_domain
from django.utils.encoding import force_text
from bitfield import BitField
@ -151,6 +153,7 @@ class Realm(models.Model):
show_digest_email = models.BooleanField(default=True) # type: bool
name_changes_disabled = models.BooleanField(default=False) # type: bool
email_changes_disabled = models.BooleanField(default=False) # type: bool
disallow_disposable_email_addresses = models.BooleanField(default=True) # type: bool
description = models.TextField(null=True) # type: Optional[Text]
send_welcome_emails = models.BooleanField(default=True) # type: bool
@ -196,6 +199,7 @@ class Realm(models.Model):
create_stream_by_admins_only=bool,
default_language=Text,
description=Text,
disallow_disposable_email_addresses=bool,
email_changes_disabled=bool,
invite_required=bool,
invite_by_admins_only=bool,
@ -386,6 +390,11 @@ def email_allowed_for_realm(email: Text, realm: Realm) -> bool:
return True
return False
def disposable_email_check(realm: Realm, email: Text) -> None:
if not realm.restricted_to_domain and realm.disallow_disposable_email_addresses:
if is_disposable_domain(email_to_domain(email)):
raise ValidationError("Please use your real email address.")
def get_realm_domains(realm: Realm) -> List[Dict[str, Text]]:
return list(realm.realmdomain_set.values('domain', 'allow_subdomains'))

View File

@ -118,6 +118,7 @@ class HomeTest(ZulipTestCase):
"realm_default_stream_groups",
"realm_default_streams",
"realm_description",
"realm_disallow_disposable_email_addresses",
"realm_domains",
"realm_email_auth_enabled",
"realm_email_changes_disabled",

View File

@ -791,7 +791,32 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
result = self.submit_reg_form_for_user("foo@example.com", "password")
self.assertEqual(result.status_code, 200)
self.assert_in_response("only allows users with e-mail", result)
self.assert_in_response("only allows users with email addresses", result)
def test_disposable_emails_before_closing(self) -> None:
"""
If you invite someone with a disposable email when
`disallow_disposable_email_addresses = False`, but
later changes to true, the invitation should succeed
but the invitee's signup attempt should fail.
"""
zulip_realm = get_realm("zulip")
zulip_realm.restricted_to_domain = False
zulip_realm.disallow_disposable_email_addresses = False
zulip_realm.save()
self.login(self.example_email("hamlet"))
external_address = "foo@mailnator.com"
self.assert_json_success(self.invite(external_address, ["Denmark"]))
self.check_sent_emails([external_address])
zulip_realm.disallow_disposable_email_addresses = True
zulip_realm.save()
result = self.submit_reg_form_for_user("foo@mailnator.com", "password")
self.assertEqual(result.status_code, 200)
self.assert_in_response("Please sign up using a real email address.", result)
def test_invite_with_non_ascii_streams(self) -> None:
"""
@ -1841,6 +1866,18 @@ class UserSignUpTest(ZulipTestCase):
self.assertIn("Your email address, {}, is not in one of the domains".format(email),
form.errors['email'][0])
def test_failed_signup_due_to_disposable_email(self) -> None:
realm = get_realm('zulip')
realm.restricted_to_domain = False
realm.disallow_disposable_email_addresses = True
realm.save()
request = HostRequestMock(host = realm.host)
request.session = {} # type: ignore
email = 'abc@mailnator.com'
form = HomepageForm({'email': email}, realm=realm)
self.assertIn("Please use your real email address", form.errors['email'][0])
def test_failed_signup_due_to_invite_required(self) -> None:
realm = get_realm('zulip')
realm.invite_required = True

View File

@ -105,7 +105,7 @@ class TemplateTestCase(ZulipTestCase):
'zilencer/enterprise_tos_accept_body.txt',
'zerver/zulipchat_migration_tos.html',
'zilencer/enterprise_tos_accept_body.txt',
'zerver/closed_realm.html',
'zerver/invalid_email.html',
'zerver/topic_is_muted.html',
'zerver/bankruptcy.html',
'zerver/lightbox_overlay.html',

View File

@ -181,6 +181,7 @@ class AdminCreateUserTest(ZulipTestCase):
admin = self.example_user('hamlet')
admin_email = admin.email
realm = admin.realm
self.login(admin_email)
do_change_is_admin(admin, True)
@ -223,8 +224,6 @@ class AdminCreateUserTest(ZulipTestCase):
"Email 'romeo@not-zulip.com' not allowed in this organization")
RealmDomain.objects.create(realm=get_realm('zulip'), domain='zulip.net')
# HAPPY PATH STARTS HERE
valid_params = dict(
email='romeo@zulip.net',
password='xxxx',
@ -239,12 +238,20 @@ class AdminCreateUserTest(ZulipTestCase):
self.assertEqual(new_user.full_name, 'Romeo Montague')
self.assertEqual(new_user.short_name, 'Romeo')
# One more error condition to test--we can't create
# the same user twice.
# we can't create the same user twice.
result = self.client_post("/json/users", valid_params)
self.assert_json_error(result,
"Email 'romeo@zulip.net' already in use")
# Don't allow user to sign up with disposable email.
realm.restricted_to_domain = False
realm.disallow_disposable_email_addresses = True
realm.save()
valid_params["email"] = "abc@mailnator.com"
result = self.client_post("/json/users", valid_params)
self.assert_json_error(result, "Disposable emails are not allowed for realm 'zulip'")
class UserProfileTest(ZulipTestCase):
def test_get_emails_from_user_ids(self) -> None:
hamlet = self.example_user('hamlet')

View File

@ -30,6 +30,7 @@ def update_realm(
name: Optional[str]=REQ(validator=check_string, default=None),
description: Optional[str]=REQ(validator=check_string, default=None),
restricted_to_domain: Optional[bool]=REQ(validator=check_bool, default=None),
disallow_disposable_email_addresses: Optional[bool]=REQ(validator=check_bool, default=None),
invite_required: Optional[bool]=REQ(validator=check_bool, default=None),
invite_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None),
name_changes_disabled: Optional[bool]=REQ(validator=check_bool, default=None),

View File

@ -14,7 +14,7 @@ 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
get_realm, get_user, get_default_stream_groups, disposable_email_check
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, \
@ -88,8 +88,14 @@ def accounts_register(request: HttpRequest) -> HttpResponse:
request, ConfirmationKeyException(ConfirmationKeyException.DOES_NOT_EXIST))
if not email_allowed_for_realm(email, realm):
return render(request, "zerver/closed_realm.html",
context={"closed_domain_name": realm.name})
return render(request, "zerver/invalid_email.html",
context={"realm_name": realm.name, "closed_domain": True})
try:
disposable_email_check(realm, email)
except ValidationError:
return render(request, "zerver/invalid_email.html",
context={"realm_name": realm.name, "disposable_emails_not_allowed": True})
if realm.deactivated:
# The user is trying to register for a deactivated realm. Advise them to

View File

@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
from django.shortcuts import redirect, render
from django.conf import settings
from django.core.exceptions import ValidationError
from zerver.decorator import require_realm_admin, zulip_login_required
from zerver.forms import CreateUserForm
@ -30,7 +31,8 @@ from zerver.lib.users import check_valid_bot_type, check_bot_creation_policy, \
check_full_name, check_short_name, check_valid_interface_type, check_valid_bot_config
from zerver.lib.utils import generate_random_token
from zerver.models import UserProfile, Stream, Message, email_allowed_for_realm, \
get_user_profile_by_id, get_user, Service, get_user_including_cross_realm
get_user_profile_by_id, get_user, Service, get_user_including_cross_realm, \
disposable_email_check
from zerver.lib.create_user import random_api_key
@ -460,6 +462,11 @@ def create_user_backend(request: HttpRequest, user_profile: UserProfile,
return json_error(_("Email '%(email)s' not allowed in this organization") %
{'email': email})
try:
disposable_email_check(realm, email)
except ValidationError:
return json_error(_("Disposable emails are not allowed for realm '{}'".format(realm.string_id)))
try:
get_user(email, user_profile.realm)
return json_error(_("Email '%s' already in use") % (email,))