diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 9f4ab6aba9..c14206bbef 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -150,6 +150,8 @@ SESSION_SAVE_EVERY_REQUEST = ENV_TOKENS.get('SESSION_SAVE_EVERY_REQUEST', SESSIO # social sharing settings SOCIAL_SHARING_SETTINGS = ENV_TOKENS.get('SOCIAL_SHARING_SETTINGS', SOCIAL_SHARING_SETTINGS) +REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') + # allow for environments to specify what cookie name our login subsystem should use # this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can # happen with some browsers (e.g. Firefox) diff --git a/cms/envs/common.py b/cms/envs/common.py index b7b0c0c76e..9b270100c6 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -32,7 +32,7 @@ import lms.envs.common from lms.envs.common import ( USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, DATA_DIR, ALL_LANGUAGES, WIKI_ENABLED, update_module_store_settings, ASSET_IGNORE_REGEX, COPYRIGHT_YEAR, - PARENTAL_CONSENT_AGE_LIMIT, COMPREHENSIVE_THEME_DIR, + PARENTAL_CONSENT_AGE_LIMIT, COMPREHENSIVE_THEME_DIR, REGISTRATION_EMAIL_PATTERNS_ALLOWED, # The following PROFILE_IMAGE_* settings are included as they are # indirectly accessed through the email opt-in API, which is # technically accessible through the CMS via legacy URLs. diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py index 41859bcdf0..dd3e462531 100644 --- a/common/djangoapps/student/forms.py +++ b/common/djangoapps/student/forms.py @@ -2,6 +2,7 @@ Utility functions for validating forms """ from importlib import import_module +import re from django import forms from django.forms import widgets @@ -17,6 +18,7 @@ from django.template import loader from django.conf import settings from microsite_configuration import microsite +from student.models import CourseEnrollmentAllowed from util.password_policy_validators import ( validate_password_length, validate_password_complexity, @@ -227,6 +229,22 @@ class AccountCreationForm(forms.Form): raise ValidationError(_("Password: ") + "; ".join(err.messages)) return password + def clean_email(self): + """ Enforce email restrictions (if applicable) """ + email = self.cleaned_data["email"] + if settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED is not None: + # This Open edX instance has restrictions on what email addresses are allowed. + allowed_patterns = settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED + # We append a '$' to the regexs to prevent the common mistake of using a + # pattern like '.*@edx\\.org' which would match 'bob@edx.org.badguy.com' + if not any(re.match(pattern + "$", email) for pattern in allowed_patterns): + # This email is not on the whitelist of allowed emails. Check if + # they may have been manually invited by an instructor and if not, + # reject the registration. + if not CourseEnrollmentAllowed.objects.filter(email=email).exists(): + raise ValidationError(_("Unauthorized email address.")) + return email + def clean_year_of_birth(self): """ Parse year_of_birth to an integer, but just use None instead of raising diff --git a/common/djangoapps/student/tests/test_create_account.py b/common/djangoapps/student/tests/test_create_account.py index 8bcf23c911..232091d19d 100644 --- a/common/djangoapps/student/tests/test_create_account.py +++ b/common/djangoapps/student/tests/test_create_account.py @@ -373,6 +373,36 @@ class TestCreateAccountValidation(TestCase): params["email"] = "not_an_email_address" assert_email_error("A properly formatted e-mail is required") + @override_settings( + REGISTRATION_EMAIL_PATTERNS_ALLOWED=[ + r'.*@edx.org', # Naive regex omitting '^', '$' and '\.' should still work. + r'^.*@(.*\.)?example\.com$', + r'^(^\w+\.\w+)@school.tld$', + ] + ) + @ddt.data( + ('bob@we-are.bad', False), + ('bob@edx.org.we-are.bad', False), + ('staff@edx.org', True), + ('student@example.com', True), + ('student@sub.example.com', True), + ('mr.teacher@school.tld', True), + ('student1234@school.tld', False), + ) + @ddt.unpack + def test_email_pattern_requirements(self, email, expect_success): + """ + Test the REGISTRATION_EMAIL_PATTERNS_ALLOWED setting, a feature which + can be used to only allow people register if their email matches a + against a whitelist of regexs. + """ + params = dict(self.minimal_params) + params["email"] = email + if expect_success: + self.assert_success(params) + else: + self.assert_error(params, "email", "Unauthorized email address.") + def test_password(self): params = dict(self.minimal_params) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 192163012b..4251611105 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -159,6 +159,7 @@ SESSION_SAVE_EVERY_REQUEST = ENV_TOKENS.get('SESSION_SAVE_EVERY_REQUEST', SESSIO REGISTRATION_EXTRA_FIELDS = ENV_TOKENS.get('REGISTRATION_EXTRA_FIELDS', REGISTRATION_EXTRA_FIELDS) REGISTRATION_EXTENSION_FORM = ENV_TOKENS.get('REGISTRATION_EXTENSION_FORM', REGISTRATION_EXTENSION_FORM) +REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') # Set the names of cookies shared with the marketing site # These have the same cookie domain as the session, which in production diff --git a/lms/envs/common.py b/lms/envs/common.py index 4adee7c0d1..20ecfe5608 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2136,6 +2136,10 @@ REGISTRATION_EXTRA_FIELDS = { 'country': 'hidden', } +# Optional setting to restrict registration / account creation to only emails +# that match a regex in this list. Set to None to allow any email (default). +REGISTRATION_EMAIL_PATTERNS_ALLOWED = None + ########################## CERTIFICATE NAME ######################## CERT_NAME_SHORT = "Certificate" CERT_NAME_LONG = "Certificate of Achievement"