diff --git a/zerver/forms.py b/zerver/forms.py index 2c4061741f..f9777dc0d7 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -16,6 +16,7 @@ from jinja2 import Markup as mark_safe from zerver.lib.actions import do_change_password, email_not_system_bot, \ validate_email_not_already_in_realm +from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.name_restrictions import is_reserved_subdomain, is_disposable_domain from zerver.lib.rate_limiter import RateLimited, get_rate_limit_result_from_request, \ RateLimitedObject, rate_limit_entity @@ -25,7 +26,7 @@ from zerver.lib.subdomains import get_subdomain, is_root_domain_available from zerver.lib.users import check_full_name from zerver.models import Realm, get_user_by_delivery_email, UserProfile, get_realm, \ email_to_domain, \ - email_allowed_for_realm, DisposableEmailError, DomainNotAllowedForRealmError, \ + DisposableEmailError, DomainNotAllowedForRealmError, \ EmailContainsPlusError from zproject.backends import email_auth_enabled, email_belongs_to_ldap, check_password_strength diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 0e25076666..90e0330d88 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -100,7 +100,7 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, ScheduledEmail, MAX_TOPIC_NAME_LENGTH, \ MAX_MESSAGE_LENGTH, get_client, get_stream, \ get_user_profile_by_id, PreregistrationUser, \ - get_realm_email_validator, email_to_username, \ + email_to_username, \ get_user_by_delivery_email, get_stream_cache_key, active_non_guest_user_ids, \ UserActivityInterval, active_user_ids, get_active_streams, \ realm_filters_for_realm, RealmFilter, stream_name_in_use, \ @@ -117,6 +117,7 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, from zerver.lib.alert_words import get_alert_word_automaton from zerver.lib.avatar import avatar_url, avatar_url_from_dict +from zerver.lib.email_validation import get_realm_email_validator from zerver.lib.stream_recipient import StreamRecipientMap from zerver.lib.validator import check_widget_content from zerver.lib.widget import do_widget_post_save_actions diff --git a/zerver/lib/email_validation.py b/zerver/lib/email_validation.py new file mode 100644 index 0000000000..c384de0a9f --- /dev/null +++ b/zerver/lib/email_validation.py @@ -0,0 +1,86 @@ +from typing import Callable + +from zerver.lib.name_restrictions import is_disposable_domain + +# TODO: Move DisposableEmailError, etc. into here. +from zerver.models import ( + email_to_username, + email_to_domain, + DisposableEmailError, + DomainNotAllowedForRealmError, + EmailContainsPlusError, + Realm, + RealmDomain, +) + +def validate_disposable(email: str) -> None: + if is_disposable_domain(email_to_domain(email)): + raise DisposableEmailError + +def get_realm_email_validator(realm: Realm) -> Callable[[str], None]: + if not realm.emails_restricted_to_domains: + # Should we also do '+' check for non-resticted realms? + if realm.disallow_disposable_email_addresses: + return validate_disposable + + # allow any email through + return lambda email: None + + ''' + RESTRICTIVE REALMS: + + Some realms only allow emails within a set + of domains that are configured in RealmDomain. + + We get the set of domains up front so that + folks can validate multiple emails without + multiple round trips to the database. + ''' + + query = RealmDomain.objects.filter(realm=realm) + rows = list(query.values('allow_subdomains', 'domain')) + + allowed_domains = { + r['domain'] for r in rows + } + + allowed_subdomains = { + r['domain'] for r in rows + if r['allow_subdomains'] + } + + def validate(email: str) -> None: + ''' + We don't have to do a "disposable" check for restricted + domains, since the realm is already giving us + a small whitelist. + ''' + + if '+' in email_to_username(email): + raise EmailContainsPlusError + + domain = email_to_domain(email) + + if domain in allowed_domains: + return + + while len(domain) > 0: + subdomain, sep, domain = domain.partition('.') + if domain in allowed_subdomains: + return + + raise DomainNotAllowedForRealmError + + return validate + +# Is a user with the given email address allowed to be in the given realm? +# (This function does not check whether the user has been invited to the realm. +# So for invite-only realms, this is the test for whether a user can be invited, +# not whether the user can sign up currently.) +def email_allowed_for_realm(email: str, realm: Realm) -> None: + ''' + Avoid calling this in a loop! + Instead, call get_realm_email_validator() + outside of the loop. + ''' + get_realm_email_validator(realm)(email) diff --git a/zerver/management/commands/generate_invite_links.py b/zerver/management/commands/generate_invite_links.py index 9a0cf74657..d6fdbe05fa 100644 --- a/zerver/management/commands/generate_invite_links.py +++ b/zerver/management/commands/generate_invite_links.py @@ -5,8 +5,8 @@ from django.core.management.base import CommandError from confirmation.models import Confirmation, create_confirmation_link from zerver.lib.management import ZulipBaseCommand -from zerver.models import DomainNotAllowedForRealmError, PreregistrationUser, \ - email_allowed_for_realm +from zerver.models import DomainNotAllowedForRealmError, PreregistrationUser +from zerver.lib.email_validation import email_allowed_for_realm class Command(ZulipBaseCommand): diff --git a/zerver/models.py b/zerver/models.py index ef29c3705e..3537e98a5b 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -30,7 +30,6 @@ from zerver.lib import cache from zerver.lib.validator import check_int, \ check_short_string, check_long_string, validate_choice_field, check_date, \ check_url, check_list -from zerver.lib.name_restrictions import is_disposable_domain from zerver.lib.types import Validator, ExtendedValidator, \ ProfileDataElement, ProfileData, RealmUserValidator, \ ExtendedFieldElement, UserFieldElement, FieldElement, \ @@ -570,78 +569,6 @@ class DisposableEmailError(Exception): class EmailContainsPlusError(Exception): pass -# Is a user with the given email address allowed to be in the given realm? -# (This function does not check whether the user has been invited to the realm. -# So for invite-only realms, this is the test for whether a user can be invited, -# not whether the user can sign up currently.) -def email_allowed_for_realm(email: str, realm: Realm) -> None: - ''' - Avoid calling this in a loop! - Instead, call get_realm_email_validator() - outside of the loop. - ''' - get_realm_email_validator(realm)(email) - -def validate_disposable(email: str) -> None: - if is_disposable_domain(email_to_domain(email)): - raise DisposableEmailError - -def get_realm_email_validator(realm: Realm) -> Callable[[str], None]: - if not realm.emails_restricted_to_domains: - # Should we also do '+' check for non-resticted realms? - if realm.disallow_disposable_email_addresses: - return validate_disposable - - # allow any email through - return lambda email: None - - ''' - RESTRICTIVE REALMS: - - Some realms only allow emails within a set - of domains that are configured in RealmDomain. - - We get the set of domains up front so that - folks can validate multiple emails without - multiple round trips to the database. - ''' - - query = RealmDomain.objects.filter(realm=realm) - rows = list(query.values('allow_subdomains', 'domain')) - - allowed_domains = { - r['domain'] for r in rows - } - - allowed_subdomains = { - r['domain'] for r in rows - if r['allow_subdomains'] - } - - def validate(email: str) -> None: - ''' - We don't have to do a "disposable" check for restricted - domains, since the realm is already giving us - a small whitelist. - ''' - - if '+' in email_to_username(email): - raise EmailContainsPlusError - - domain = email_to_domain(email) - - if domain in allowed_domains: - return - - while len(domain) > 0: - subdomain, sep, domain = domain.partition('.') - if domain in allowed_subdomains: - return - - raise DomainNotAllowedForRealmError - - return validate - def get_realm_domains(realm: Realm) -> List[Dict[str, str]]: return list(realm.realmdomain_set.values('domain', 'allow_subdomains')) diff --git a/zerver/tests/test_realm_domains.py b/zerver/tests/test_realm_domains.py index 2d822f7c52..9ab11bf988 100644 --- a/zerver/tests/test_realm_domains.py +++ b/zerver/tests/test_realm_domains.py @@ -6,9 +6,10 @@ from django.db.utils import IntegrityError from zerver.lib.actions import do_change_is_admin, \ do_change_realm_domain, do_create_realm, \ do_remove_realm_domain +from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.domains import validate_domain from zerver.lib.test_classes import ZulipTestCase -from zerver.models import email_allowed_for_realm, get_realm, \ +from zerver.models import get_realm, \ RealmDomain, DomainNotAllowedForRealmError import ujson diff --git a/zerver/views/registration.py b/zerver/views/registration.py index 2b50f6be5e..ba7de5c49b 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -11,10 +11,11 @@ from django.core.exceptions import ValidationError from django.core import validators from zerver.context_processors import get_realm_from_request, login_context from zerver.models import UserProfile, Realm, Stream, MultiuseInvite, \ - name_changes_disabled, email_to_username, email_allowed_for_realm, \ + name_changes_disabled, email_to_username, \ get_realm, get_user_by_delivery_email, get_default_stream_groups, DisposableEmailError, \ DomainNotAllowedForRealmError, get_source_profile, EmailContainsPlusError, \ PreregistrationUser +from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.send_email import send_email, FromAddress from zerver.lib.actions import do_change_password, do_change_full_name, \ do_activate_user, do_create_user, do_create_realm, \ diff --git a/zerver/views/users.py b/zerver/views/users.py index 16015a60d0..5436760af4 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -17,6 +17,7 @@ from zerver.lib.actions import do_change_avatar_fields, do_change_bot_owner, \ do_update_user_custom_profile_data_if_changed, check_remove_custom_profile_field_value from zerver.lib.avatar import avatar_url, get_gravatar_url from zerver.lib.bot_config import set_bot_config +from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.exceptions import CannotDeactivateLastUserError from zerver.lib.integrations import EMBEDDED_BOTS from zerver.lib.request import has_request_variables, REQ @@ -29,7 +30,7 @@ from zerver.lib.users import check_valid_bot_type, check_bot_creation_policy, \ access_bot_by_id, add_service, access_user_by_id, check_bot_name_available, \ validate_user_custom_profile_data, get_raw_user_data, get_api_key from zerver.lib.utils import generate_api_key, generate_random_token -from zerver.models import UserProfile, Stream, Message, email_allowed_for_realm, \ +from zerver.models import UserProfile, Stream, Message, \ get_user_by_delivery_email, Service, get_user_including_cross_realm, \ DomainNotAllowedForRealmError, DisposableEmailError, get_user_profile_by_id_in_realm, \ EmailContainsPlusError, get_user_by_id_in_realm_including_cross_realm, Realm, \ diff --git a/zproject/backends.py b/zproject/backends.py index a843f8d7bf..00a7ea31d4 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -54,6 +54,7 @@ from zerver.lib.actions import do_create_user, do_reactivate_user, do_deactivate from zerver.lib.avatar import is_avatar_new, avatar_url from zerver.lib.avatar_hash import user_avatar_content_hash from zerver.lib.dev_ldap_directory import init_fakeldap +from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.mobile_auth_otp import is_valid_otp from zerver.lib.rate_limiter import clear_history, rate_limit_request_by_entity, RateLimitedObject from zerver.lib.request import JsonableError @@ -61,7 +62,7 @@ from zerver.lib.users import check_full_name, validate_user_custom_profile_field from zerver.lib.redis_utils import get_redis_client, get_dict_from_redis, put_dict_in_redis from zerver.models import CustomProfileField, DisposableEmailError, DomainNotAllowedForRealmError, \ EmailContainsPlusError, PreregistrationUser, UserProfile, Realm, custom_profile_fields_for_realm, \ - email_allowed_for_realm, get_user_profile_by_id, remote_user_to_email, \ + get_user_profile_by_id, remote_user_to_email, \ email_to_username, get_realm, get_user_by_delivery_email, supported_auth_backends redis_client = get_redis_client()