Added support for Two-factor authentication for improving security. Fixes #6543

This commit is contained in:
Ashesh Vashi
2021-12-02 16:47:18 +05:30
committed by Akshay Joshi
parent fe096116be
commit 36c9eb3dfd
56 changed files with 2770 additions and 119 deletions

View File

@@ -0,0 +1,154 @@
##############################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##############################################################################
from pgadmin.authenticate.mfa import mfa_enabled
import config
__MFA_ENABLED = 'MFA Enabled'
__MFA_DISABLED = 'MFA Disabled'
TEST_UTILS_AUTH_PKG = 'tests.utils'
def __mfa_is_enabled():
return __MFA_ENABLED
def __mfa_is_disabled():
return __MFA_DISABLED
def check_mfa_enabled(test):
config.MFA_ENABLED = test.enabled
config.MFA_SUPPORTED_METHODS = test.supported_list
if mfa_enabled(__mfa_is_enabled, __mfa_is_disabled) != test.expected:
test.fail(test.fail_msg)
def log_message_in_init_app(test):
import types
from unittest.mock import patch
from .. import init_app
from .utils import test_create_dummy_app
auth_method_msg = "'xyz' is not a valid multi-factor authentication method"
disabled_msg = \
"No valid multi-factor authentication found, hence - disabling it."
warning_invalid_auth_found = False
warning_disable_auth = False
dummy_app = test_create_dummy_app(test.name)
def _log_warning_msg(_msg):
nonlocal warning_invalid_auth_found
nonlocal warning_disable_auth
if auth_method_msg == _msg:
warning_invalid_auth_found = True
return
if _msg == disabled_msg:
warning_disable_auth = True
with patch.object(
dummy_app.logger,
'warning',
new=_log_warning_msg
):
config.MFA_ENABLED = True
config.MFA_SUPPORTED_METHODS = test.supported_list
init_app(dummy_app)
if warning_invalid_auth_found is not test.warning_invalid_auth_found \
or warning_disable_auth is not test.warning_disable_auth:
test.fail(test.fail_msg)
test.fail()
config_scenarios = [
(
"Check MFA enabled with no authenticators?",
dict(
check=check_mfa_enabled, enabled=True, supported_list=list(),
expected=__MFA_DISABLED,
fail_msg="MFA is enabled with no authenticators, but - "
"'execute_if_disabled' function is not called."
),
),
(
"Check MFA enabled?",
dict(
check=check_mfa_enabled, enabled=True,
supported_list=[TEST_UTILS_AUTH_PKG], expected=__MFA_ENABLED,
fail_msg="MFA is enable, but - 'execute_if_enabled' function "
"is not called."
),
),
(
"Check MFA disabled check functionality works?",
dict(
check=check_mfa_enabled, enabled=False,
supported_list=list(),
expected=__MFA_DISABLED,
fail_msg="MFA is disabled, but - 'execute_if_enabled' function "
"is called."
),
),
(
"Check MFA in the supported MFA LIST is part of the registered one",
dict(
check=check_mfa_enabled, enabled=True,
supported_list=["not-in-list"],
expected=__MFA_DISABLED,
fail_msg="MFA is enabled with invalid authenticators, but - "
"'execute_if_enabled' function is called"
),
),
(
"Check warning message with invalid method appended during "
"init_app(...)",
dict(
check=log_message_in_init_app,
supported_list=["xyz", TEST_UTILS_AUTH_PKG],
name="warning_app_having_invalid_method",
warning_invalid_auth_found=True, warning_disable_auth=False,
fail_msg="Warning for invalid auth is not found",
),
),
(
"Check warning message with invalid method during "
"init_app(...) ",
dict(
check=log_message_in_init_app, supported_list=["xyz"],
name="warning_app_with_invalid_method",
warning_invalid_auth_found=False, warning_disable_auth=True,
fail_msg="Warning for invalid auth is not found",
),
),
(
"Check warning message when empty supported mfa list during "
"init_app(...)",
dict(
check=log_message_in_init_app, supported_list=[""],
name="warning_app_with_empty_supported_list",
warning_invalid_auth_found=False, warning_disable_auth=True,
fail_msg="Warning not found with empty supported mfa methods",
),
),
(
"No warning message should found with valid configurations during "
"init_app(...)",
dict(
check=log_message_in_init_app, name="no_warning_app",
supported_list=[TEST_UTILS_AUTH_PKG],
warning_invalid_auth_found=False, warning_disable_auth=False,
fail_msg="Warning found with valid configure",
),
),
]

View File

@@ -0,0 +1,56 @@
##############################################################################
#
# 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
import config
from .test_config import config_scenarios
from .test_user_execution import user_execution_scenarios
from .test_mfa_view import validation_view_scenarios
from .utils import init_dummy_auth_class
test_scenarios = list()
test_scenarios += config_scenarios
test_scenarios += user_execution_scenarios
test_scenarios += validation_view_scenarios
class TestMFATests(BaseTestGenerator):
scenarios = test_scenarios
@classmethod
def setUpClass(cls):
config.MFA_ENABLED = True
init_dummy_auth_class()
@classmethod
def tearDownClass(cls):
config.MFA_ENABLED = False
config.MFA_SUPPORTED_METHODS = []
def setUp(self):
config.MFA_SUPPORTED_METHODS = ['tests.utils']
start = getattr(self, 'start', None)
if start is not None:
start(self)
super(BaseTestGenerator, self).setUp()
def tearDown(self):
finish = getattr(self, 'finish', None)
if finish is not None:
finish(self)
config.MFA_SUPPORTED_METHODS = []
super(BaseTestGenerator, self).tearDown()
def runTest(self):
self.check(self)

View File

@@ -0,0 +1,66 @@
##############################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##############################################################################
from unittest.mock import patch
import config
from .utils import setup_mfa_app, MockCurrentUserId, MockUserMFA
from pgadmin.authenticate.mfa.utils import ValidationException
__MFA_PACKAGE = '.'.join((__package__.split('.'))[:-1])
__AUTH_PACKAGE = '.'.join((__package__.split('.'))[:-2])
def check_validation_view_content(test):
user_mfa_test_data = [
MockUserMFA(1, "dummy", ""),
MockUserMFA(1, "no-present-in-list", None),
]
def mock_log_exception(ex):
test.assertTrue(type(ex) == ValidationException)
with patch(
__MFA_PACKAGE + ".utils.current_user", return_value=MockCurrentUserId()
):
with patch(__MFA_PACKAGE + ".utils.UserMFA") as mock_user_mfa:
with test.app.test_request_context():
with patch("flask.current_app") as mock_current_app:
mock_user_mfa.query.filter_by.return_value \
.all.return_value = user_mfa_test_data
mock_current_app.logger.exception = mock_log_exception
with patch(__AUTH_PACKAGE + ".session") as mock_session:
session = {
'auth_source_manager': {
'current_source': getattr(
test, 'auth_method', 'internal'
)
}
}
mock_session.__getitem__.side_effect = \
session.__getitem__
response = test.tester.get("/mfa/validate")
test.assertEquals(response.status_code, 200)
test.assertEquals(
response.headers["Content-Type"], "text/html; charset=utf-8"
)
# test.assertTrue('Dummy' in response.data.decode('utf8'))
# End of test case - check_validation_view_content
validation_view_scenarios = [
(
"Validation view of a MFA method should return a HTML tags",
dict(start=setup_mfa_app, check=check_validation_view_content),
),
]

View File

@@ -0,0 +1,125 @@
##############################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##############################################################################
from unittest.mock import patch
import config
from pgadmin.authenticate.mfa.utils import \
mfa_user_force_registration_required
from pgadmin.authenticate.mfa.utils import mfa_user_registered, \
user_supported_mfa_methods
from .utils import MockUserMFA, MockCurrentUserId
__MFA_PACKAGE = '.'.join((__package__.split('.'))[:-1])
def __return_true():
return True
def __return_false():
return False
def check_user_registered(test):
user_mfa_test_data = [
MockUserMFA(1, "dummy", "Hello guys"),
MockUserMFA(1, "no-present-in-list", None),
]
with patch(
__MFA_PACKAGE + ".utils.current_user", return_value=MockCurrentUserId()
):
with patch(__MFA_PACKAGE + ".utils.UserMFA") as mock_user_mfa:
mock_user_mfa.query.filter_by.return_value.all.return_value = \
user_mfa_test_data
ret = mfa_user_registered(__return_true, __return_false)
if ret is None:
test.fail(
"User registration check has not called either "
"'is_registered' or 'is_not_registered' function"
)
if ret is False:
test.fail(
"Not expected to be called 'is_not_registered' function "
"as 'dummy' is in the supported MFA methods"
)
methods = user_supported_mfa_methods()
if "dummy" not in methods:
test.fail(
"User registration methods are not valid: {}".format(
methods
)
)
# Removed the 'dummy' from the user's registered MFA list
user_mfa_test_data.pop(0)
ret = mfa_user_registered(__return_true, __return_false)
if ret is None:
test.fail(
"User registration check has not called either "
"'is_registered' or 'is_not_registered' function"
)
if ret is True:
test.fail(
"Not expected to be called 'is_registered' function as "
"'not-present-in-list' is not a valid multi-factor "
"authentication method"
)
# End of test case - check_user_registered
def check_force_registration_required(test):
if mfa_user_force_registration_required(
__return_false, __return_true
) is None:
test.fail(
"User registration check did not call either register or "
"do_not_register function"
)
config.MFA_FORCE_REGISTRATION = False
if mfa_user_force_registration_required(
__return_true, __return_false
) is True:
test.fail(
"User registration function should not be called, when "
"config.MFA_FORCE_REGISTRATION is True"
)
config.MFA_FORCE_REGISTRATION = True
if mfa_user_force_registration_required(
__return_true, __return_false
) is False:
test.fail(
"'do_not_registration' function should not be called, when "
"config.MFA_FORCE_REGISTRATION is True"
)
# End of test case - check_force_registration_required
user_execution_scenarios = [
(
"Check user is registered to do MFA",
dict(check=check_user_registered),
),
(
"Require the forcefull registration for MFA?",
dict(check=check_force_registration_required),
),
]

View File

@@ -0,0 +1,111 @@
##############################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##############################################################################
import types
from flask import Flask, Response
import config
from pgadmin.authenticate.mfa import init_app as mfa_init_app
def init_dummy_auth_class():
from pgadmin.authenticate.mfa.registry import BaseMFAuth
class DummyAuth(BaseMFAuth): # NOSONAR - S5603
"""
A dummy authentication for testing the registry ability of adding
'dummy' authentication method.
Declaration is enough to use this class, we don't have to use it
directly, as it will be initialized automatically by the registry, and
ready to use.
"""
@property
def name(self):
return "dummy"
@property
def label(self):
return "Dummy"
def validate(self, **kwargs):
return true
def validation_view(self):
return "View"
def registration_view(self):
return "Registration"
def register_url_endpoints(self, blueprint):
print('Initialize the end-points for dummy auth')
# FPSONAR_OFF
def test_create_dummy_app(name=__name__):
import os
import pgadmin
from pgadmin.misc.themes import themes
def index():
return Response("<html><body>logged in</body></html>")
template_folder = os.path.join(
os.path.dirname(os.path.realpath(pgadmin.__file__)), 'templates'
)
app = Flask(name, template_folder=template_folder)
config.MFA_ENABLED = True
config.MFA_SUPPORTED_METHODS = ['tests.utils']
app.config.from_object(config)
app.config.update(dict(LOGIN_DISABLED=True))
app.add_url_rule("/", "index", index, methods=("GET",))
app.add_url_rule(
"/favicon.ico", "redirects.favicon", index, methods=("GET",)
)
app.add_url_rule("/browser", "browser.index", index, methods=("GET",))
app.add_url_rule("/tools", "tools.index", index, methods=("GET",))
app.add_url_rule(
"/users", "user_management.index", index, methods=("GET",)
)
app.add_url_rule(
"/login", "security.logout", index, methods=("GET",)
)
app.add_url_rule(
"/kerberos_logout", "authenticate.kerberos_logout", index,
methods=("GET",)
)
def __dummy_logout_hook(self, blueprint):
pass # We don't need the logout url when dummy auth is enabled.
app.register_logout_hook = types.MethodType(__dummy_logout_hook, app)
themes(app)
return app
def setup_mfa_app(test):
test.app = test_create_dummy_app()
mfa_init_app(test.app)
test.tester = test.app.test_client()
class MockUserMFA():
"""Mock user for UserMFA"""
def __init__(self, user_id, mfa_auth, options):
self.user_id = user_id
self.mfa_auth = mfa_auth
self.options = options
class MockCurrentUserId():
id = 1