diff --git a/web/config.py b/web/config.py index bda2f9097..1c65a3180 100644 --- a/web/config.py +++ b/web/config.py @@ -388,7 +388,9 @@ SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE = \ # flask-security-too will validate email addresses and check deliverability # by default. Disable the deliverability check by default, which was the old # behaviour in <= v5.3 -SECURITY_EMAIL_VALIDATOR_ARGS = {"check_deliverability": False} +CHECK_EMAIL_DELIVERABILITY = False +SECURITY_EMAIL_VALIDATOR_ARGS = \ + {"check_deliverability": CHECK_EMAIL_DELIVERABILITY} ########################################################################## # Upgrade checks diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 41a7e328a..8bfb18d39 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -465,6 +465,8 @@ def create_app(app_name=None): # CSRF Token expiration till session expires 'WTF_CSRF_TIME_LIMIT': getattr(config, 'CSRF_TIME_LIMIT', None), 'WTF_CSRF_METHODS': ['GET', 'POST', 'PUT', 'DELETE'], + # Disable deliverable check for email addresss + 'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS })) security.init_app(app, user_datastore) diff --git a/web/pgadmin/setup/tests/test_no_email_deliverability.py b/web/pgadmin/setup/tests/test_no_email_deliverability.py new file mode 100644 index 000000000..c54590bf9 --- /dev/null +++ b/web/pgadmin/setup/tests/test_no_email_deliverability.py @@ -0,0 +1,62 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.setup import user_info +import config +from regression.python_test_utils.test_utils import module_patch +from unittest.mock import patch +from pgadmin.utils.constants import ENTER_EMAIL_ADDRESS + + +class EmailValidationOnSetup(BaseTestGenerator): + """ + This class tests the non-deliverability of email for invalid email id. + This test case is only responsible for testing non-deliverability emails + only. + """ + PPROMPT_RETURN_VALUE = '1234567' + + scenarios = [ + # scenario for testing invalid email for non-deliverability only + ('TestCase for email validation', dict( + data=['postgres@local.dev', 'pg@pgadminrocks.com', + 'me.pg@demo.dev', 'pg@123.pgcom', + 'pg@postgres.local', 'postgres@pg.blah'], + check_deliverability=False, + )), + ] + + @patch('builtins.input') + @patch('os.environ') + def runTest(self, os_environ_mock, input_mock): + + if config.SERVER_MODE is False: + self.skipTest( + "Can not email validation test cases in the DESKTOP mode." + ) + + os_environ_mock.return_value = [] + config.CHECK_EMAIL_DELIVERABILITY = self.check_deliverability + + with module_patch('pgadmin.setup.user_info.pprompt') as pprompt_mock: + pprompt_mock.return_value \ + = self.PPROMPT_RETURN_VALUE, self.PPROMPT_RETURN_VALUE + + for e in self.data: + input_mock.return_value = e + # skipping some setup-db part as we are only testing the + # mail validation through setup. + email, password = user_info() + + input_mock.assert_called_once_with(ENTER_EMAIL_ADDRESS) + # assert equal means deliverability is not done, and entered + # email id is returned as it is. + self.assertEqual(e, email) + input_mock.reset_mock() diff --git a/web/pgadmin/setup/user_info.py b/web/pgadmin/setup/user_info.py index 510d255ae..14a0f887b 100644 --- a/web/pgadmin/setup/user_info.py +++ b/web/pgadmin/setup/user_info.py @@ -13,6 +13,7 @@ import random import os import re import getpass +from pgadmin.utils.constants import ENTER_EMAIL_ADDRESS from pgadmin.utils.validation_utils import validate_email @@ -27,6 +28,10 @@ def user_info_desktop(): return email, p1 +def pprompt(): + return getpass.getpass(), getpass.getpass('Retype password:') + + def user_info_server(): print("NOTE: Configuring authentication for SERVER mode.\n") @@ -45,13 +50,10 @@ def user_info_server(): "pgAdmin user account:\n" ) - email = input("Email address: ") + email = input(ENTER_EMAIL_ADDRESS) while not validate_email(email): print('Invalid email address. Please try again.') - email = input("Email address: ") - - def pprompt(): - return getpass.getpass(), getpass.getpass('Retype password:') + email = input(ENTER_EMAIL_ADDRESS) p1, p2 = pprompt() while p1 != p2 or len(p1) < 6: diff --git a/web/pgadmin/tools/user_management/tests/__init__.py b/web/pgadmin/tools/user_management/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/tools/user_management/tests/test_validate_user_email.py b/web/pgadmin/tools/user_management/tests/test_validate_user_email.py new file mode 100644 index 000000000..043c2538b --- /dev/null +++ b/web/pgadmin/tools/user_management/tests/test_validate_user_email.py @@ -0,0 +1,62 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.tools.user_management import validate_user +from unittest.mock import patch +import config + + +class TestValidateUser(BaseTestGenerator): + """ This class will test the user email validation with/without email + deliverability while validating user. """ + + scenarios = [ + ('User email validation (no deliverability)', + dict( + data=dict( + email='postgres@local.dev', + check_deliverability=False, + expected_data=dict( + test_result='postgres@local.dev' + ) + ) + )), + ('User email validation (with deliverability)', + dict( + data=dict( + email='postgres@local.dev', + check_deliverability=True, + expected_data=dict( + test_result='Invalid email address.' + ) + ) + )) + ] + + @patch('pgadmin.tools.user_management.validate_password') + def runTest(self, validate_password_mock): + + if config.SERVER_MODE is False: + self.skipTest( + "Can not email validation test cases in the DESKTOP mode." + ) + config.CHECK_EMAIL_DELIVERABILITY = self.data['check_deliverability'] + ndata = {} + + validate_password_mock.return_value.method.return_value = '' + try: + ndata = validate_user(self.data) + except Exception as e: + ndata['email'] = str(e.description) + + # assert equal means deliverability is not done, and entered + # email id is returned as it is. + self.assertEqual(ndata['email'], + self.data['expected_data']['test_result']) diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 0800193b4..67cc690e9 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -98,3 +98,5 @@ BINARY_PATHS = { } UTILITIES_ARRAY = ['pg_dump', 'pg_dumpall', 'pg_restore', 'psql'] + +ENTER_EMAIL_ADDRESS = "Email address: " diff --git a/web/pgadmin/utils/tests/test_validate_email.py b/web/pgadmin/utils/tests/test_validate_email.py new file mode 100644 index 000000000..730b0f81f --- /dev/null +++ b/web/pgadmin/utils/tests/test_validate_email.py @@ -0,0 +1,82 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.utils.validation_utils import validate_email +from unittest.mock import patch +import config + + +class TestEmailValidate(BaseTestGenerator): + """ This class will test the email validation utility with or without email + deliverability. """ + + scenarios = [ + ('Email validation (no deliverability)', + dict( + data=dict( + email_list=['postgres@local.dev', 'pg@pgadminrocks.com', + 'me.pg@demo.dev', 'pg@123.pgcom', + 'pg@postgres.local', 'postgres@pg.blah', + 'john@doe.com', 'punster@tr.co', + 'admin@example.com'], + check_deliverability=False, + expected_data=dict( + test_result=True + ) + ) + )), + ('Email validation (with deliverability)', + dict( + data=dict( + email_list=['postgres@local.dev', 'pg@pgadminrocks.com', + 'pg@postgres.local'], + check_deliverability=True, + expected_data=dict( + test_result=False + ) + ) + )), + ('Empty email validation (no deliverability)', + dict( + data=dict( + email_list=[''], + check_deliverability=False, + expected_data=dict( + test_result=False + ) + ) + )), + ('Empty email validation (with deliverability)', + dict( + data=dict( + email_list=[''], + check_deliverability=True, + expected_data=dict( + test_result=False + ) + ) + )) + ] + + def runTest(self): + + if config.SERVER_MODE is False: + self.skipTest( + "Can not run email validation test cases in the DESKTOP mode." + ) + config.CHECK_EMAIL_DELIVERABILITY = self.data['check_deliverability'] + + for e in self.data['email_list']: + result = validate_email(e) + # validate_email returns True if email is valid, + # even if non-deliverable. False if email is not valid or + # deliverability is turned ON. + self.assertEqual(result, + self.data['expected_data']['test_result']) diff --git a/web/pgadmin/utils/validation_utils.py b/web/pgadmin/utils/validation_utils.py index 500146ea5..e9b4fad8d 100644 --- a/web/pgadmin/utils/validation_utils.py +++ b/web/pgadmin/utils/validation_utils.py @@ -7,20 +7,21 @@ # ########################################################################## -import re +from email_validator import validate_email as email_validate, \ + EmailNotValidError +import config def validate_email(email): - if email == '' or email is None: + try: + # Validate. + valid = email_validate( + email, check_deliverability=config.CHECK_EMAIL_DELIVERABILITY) + + # Update with the normalized form. + email = valid.email + return True + except EmailNotValidError as e: + # email is not valid, exception message is human-readable + print(str(e)) return False - - email_filter = re.compile( - "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9]" - "(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]" - "(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" - ) - - if not email_filter.match(email): - return False - - return True diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index c9120ddaf..9dba4cf06 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -17,6 +17,7 @@ import sqlite3 import shutil from functools import partial import random +import importlib from selenium.webdriver.support.wait import WebDriverWait from testtools.testcase import clone_test_with_new_id @@ -1747,3 +1748,34 @@ def create_users_for_parallel_tests(tester): user_id = json.loads(response.data.decode('utf-8'))['id'] user_details['user_id'] = user_id return user_details + + +def module_patch(*args): + """ + This is a helper function responsible to import a function inside + a module with the same name + + e.g. user_info module has user_info function in it. + + :param args: + :return: + """ + + target = args[0] + components = target.split('.') + from unittest import mock + for i in range(len(components), 0, -1): + try: + # attempt to import the module + imported = importlib.import_module('.'.join(components[:i])) + + # module was imported, let's use it in the patch + patch = mock.patch(*args) + patch.getter = lambda: imported + patch.attribute = '.'.join(components[i:]) + return patch + except Exception as exc: + pass + + # did not find a module, just return the default mock + return mock.patch(*args)