From 36c9eb3dfd9202c5ab46272d573619c75cfdd79a Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Thu, 2 Dec 2021 16:47:18 +0530 Subject: [PATCH] Added support for Two-factor authentication for improving security. Fixes #6543 --- .dockerignore | 6 + Dockerfile | 4 + docs/en_US/getting_started.rst | 1 + docs/en_US/images/mfa_registration.png | Bin 0 -> 147202 bytes docs/en_US/mfa.rst | 57 +++ docs/en_US/release_notes_6_3.rst | 1 + pkg/mac/build-functions.sh | 11 +- requirements.txt | 3 + web/.eslintignore | 1 + web/config.py | 23 + web/migrations/script.py.mako | 3 +- web/migrations/versions/15c88f765bc8_.py | 44 ++ web/migrations/versions/6650c52670c2_.py | 10 +- web/pgadmin/authenticate/__init__.py | 22 +- web/pgadmin/authenticate/mfa/__init__.py | 110 +++++ web/pgadmin/authenticate/mfa/authenticator.py | 222 ++++++++++ web/pgadmin/authenticate/mfa/email.py | 310 +++++++++++++ web/pgadmin/authenticate/mfa/registry.py | 167 +++++++ .../mfa/static/images/email_lock.svg | 5 + .../mfa/static/images/totp_lock.svg | 5 + .../authenticate/mfa/templates/mfa/email.js | 66 +++ .../mfa/templates/mfa/email_code_sent.html | 19 + .../mfa/templates/mfa/email_view.html | 7 + .../mfa/templates/mfa/register.html | 78 ++++ .../mfa/templates/mfa/validate.html | 121 ++++++ .../security/email/send_email_otp.html | 2 + .../security/email/send_email_otp.txt | 2 + .../authenticate/mfa/tests/test_config.py | 154 +++++++ .../authenticate/mfa/tests/test_mfa.py | 56 +++ .../authenticate/mfa/tests/test_mfa_view.py | 66 +++ .../mfa/tests/test_user_execution.py | 125 ++++++ web/pgadmin/authenticate/mfa/tests/utils.py | 111 +++++ web/pgadmin/authenticate/mfa/utils.py | 408 ++++++++++++++++++ web/pgadmin/authenticate/mfa/views.py | 346 +++++++++++++++ web/pgadmin/browser/__init__.py | 12 +- web/pgadmin/browser/static/js/browser.js | 4 +- web/pgadmin/browser/static/js/dialog.js | 110 +++++ .../browser/templates/browser/index.html | 11 + web/pgadmin/model/__init__.py | 8 + web/pgadmin/static/img/login.svg | 2 +- web/pgadmin/static/js/pgadmin.js | 2 + web/pgadmin/static/scss/_pgadmin.style.scss | 26 +- .../scss/resources/_default.variables.scss | 1 + .../scss/resources/dark/_theme.variables.scss | 2 + .../high_contrast/_theme.variables.scss | 2 + web/pgadmin/templates/base.html | 6 +- .../templates/security/change_password.html | 4 +- web/pgadmin/templates/security/panel.html | 25 +- web/pgadmin/tools/datagrid/__init__.py | 2 + web/pgadmin/tools/debugger/__init__.py | 2 + web/pgadmin/tools/erd/__init__.py | 2 + web/pgadmin/tools/psql/__init__.py | 2 + web/pgadmin/tools/schema_diff/__init__.py | 2 + web/pgadmin/tools/sqleditor/__init__.py | 3 +- .../static/js/user_management.js | 94 +--- web/webpack.shim.js | 1 + 56 files changed, 2770 insertions(+), 119 deletions(-) create mode 100644 docs/en_US/images/mfa_registration.png create mode 100644 docs/en_US/mfa.rst create mode 100644 web/migrations/versions/15c88f765bc8_.py create mode 100644 web/pgadmin/authenticate/mfa/__init__.py create mode 100644 web/pgadmin/authenticate/mfa/authenticator.py create mode 100644 web/pgadmin/authenticate/mfa/email.py create mode 100644 web/pgadmin/authenticate/mfa/registry.py create mode 100644 web/pgadmin/authenticate/mfa/static/images/email_lock.svg create mode 100644 web/pgadmin/authenticate/mfa/static/images/totp_lock.svg create mode 100644 web/pgadmin/authenticate/mfa/templates/mfa/email.js create mode 100644 web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html create mode 100644 web/pgadmin/authenticate/mfa/templates/mfa/email_view.html create mode 100644 web/pgadmin/authenticate/mfa/templates/mfa/register.html create mode 100644 web/pgadmin/authenticate/mfa/templates/mfa/validate.html create mode 100644 web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.html create mode 100644 web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.txt create mode 100644 web/pgadmin/authenticate/mfa/tests/test_config.py create mode 100644 web/pgadmin/authenticate/mfa/tests/test_mfa.py create mode 100644 web/pgadmin/authenticate/mfa/tests/test_mfa_view.py create mode 100644 web/pgadmin/authenticate/mfa/tests/test_user_execution.py create mode 100644 web/pgadmin/authenticate/mfa/tests/utils.py create mode 100644 web/pgadmin/authenticate/mfa/utils.py create mode 100644 web/pgadmin/authenticate/mfa/views.py create mode 100644 web/pgadmin/browser/static/js/dialog.js diff --git a/.dockerignore b/.dockerignore index 93ac96f9b..a395c4982 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,12 @@ web/node_modules web/*.log web/regression web/**/tests/ +web/**/*.pyc +web/**/__pycache__ .DS_Store web/pgadmin/messages.pot web/pgadmin/translations/??/LC_MESSAGES/messages.po +Dockerfile +docs/en_US/_build/**/* +src-build/**/* +dist/**/* diff --git a/Dockerfile b/Dockerfile index ee5c447d1..a265e6a2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,9 @@ RUN apk add --no-cache \ krb5-dev \ rust \ cargo \ + zlib-dev \ + libjpeg-turbo-dev \ + libpng-dev \ python3-dev && \ python3 -m venv --system-site-packages --without-pip /venv && \ /venv/bin/python3 -m pip install --no-cache-dir -r requirements.txt && \ @@ -177,6 +180,7 @@ RUN apk add \ py3-pip \ postfix \ krb5-libs \ + libjpeg-turbo \ shadow \ sudo \ libedit \ diff --git a/docs/en_US/getting_started.rst b/docs/en_US/getting_started.rst index 92967847e..11646b6ea 100644 --- a/docs/en_US/getting_started.rst +++ b/docs/en_US/getting_started.rst @@ -33,6 +33,7 @@ Mode is pre-configured for security. deployment login + mfa user_management change_user_password restore_locked_user diff --git a/docs/en_US/images/mfa_registration.png b/docs/en_US/images/mfa_registration.png new file mode 100644 index 0000000000000000000000000000000000000000..12b5b1759b2b0ff6deda402dc386e52184024442 GIT binary patch literal 147202 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ΠyD9!snNZB~oM{0k&DWOPRoYK_R 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 literal 0 HcmV?d00001 diff --git a/docs/en_US/mfa.rst b/docs/en_US/mfa.rst new file mode 100644 index 000000000..cf95f4063 --- /dev/null +++ b/docs/en_US/mfa.rst @@ -0,0 +1,57 @@ +.. _mfa: + +************************************************* +`Enabling two-factor authentication (2FA)`:index: +************************************************* + +About two-factor authentication +=============================== +Two-factor authentication (2FA) is an extra layer of security used when logging +into websites or apps. With 2FA, you have to log in with your username and +password and provide another form of authentication that only you know or have +access to. + + +Setup two-factor authentication +=============================== +To set up 2FA for pgAdmin 4, you must configure the Two-factor Authentication +settings in *config_local.py* or *config_system.py* (see the +:ref:`config.py ` documentation) on the system where pgAdmin is +installed in Server mode. You can copy these settings from *config.py* file and +modify the values for the following parameters. + +.. csv-table:: + :header: "**Parameter**", "**Description**" + :class: longtable + :widths: 35, 55 + + "MFA_ENABLED","The default value for this parameter is False. + To enable 2FA, set the value to *True*" + "SUPPORTED_MFA_LIST", "Set the authentication methods to be supported " + "MFA_EMAIL_SUBJECT", " - Verification Code e.g. pgAdmin 4 - + Verification Code" + "MFA_FORCE_REGISTRATION", "Force the user to configure the authentication + method on login (if no authentication is already configured)." + +*NOTE: You must set the 'Mail server settings' in config_local.py or +config_system.py in order to use 'email' as two-factor authentication method +(see the* :ref:`config.py ` *documentation).* + + +Configure two-factor authentication +=================================== +To configure 2FA for a user, you must click on 'Two-factor Authentication' +in the `User` menu in right-top corner. It will list down all the supported +multi factor authentication methods. Click on 'Setup' of one of those methods +and follow the steps for each authentication method. You will see the `Delete` +button for the authentication method, which is already been configured. +Clicking on `Delete` button will deregister the authentication method for the +current user. + +.. image:: images/mfa_registration.png + :alt: Configure two-factor authentication + :align: center + +You can also force users to configure the two-factor +authentication methods on login by setting *MFA_FORCE_REGISTRATION* parameter +to *True*. diff --git a/docs/en_US/release_notes_6_3.rst b/docs/en_US/release_notes_6_3.rst index 9d233d2ff..b52766f3d 100644 --- a/docs/en_US/release_notes_6_3.rst +++ b/docs/en_US/release_notes_6_3.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #6543 `_ - Added support for Two-factor authentication for improving security. | `Issue #6872 `_ - Include GSSAPI support in the PostgreSQL libraries and utilities on macOS. Housekeeping diff --git a/pkg/mac/build-functions.sh b/pkg/mac/build-functions.sh index 35502265b..10f27ac0f 100644 --- a/pkg/mac/build-functions.sh +++ b/pkg/mac/build-functions.sh @@ -1,10 +1,11 @@ _setup_env() { - APP_RELEASE=`grep "^APP_RELEASE" web/config.py | cut -d"=" -f2 | sed 's/ //g'` - APP_REVISION=`grep "^APP_REVISION" web/config.py | cut -d"=" -f2 | sed 's/ //g'` - APP_NAME=`grep "^APP_NAME" web/config.py | cut -d"=" -f2 | sed "s/'//g" | sed 's/^ //'` + FUNCS_DIR=$(cd `dirname $0` && pwd)/../.. + APP_RELEASE=`grep "^APP_RELEASE" ${FUNCS_DIR}/web/config.py | cut -d"=" -f2 | sed 's/ //g'` + APP_REVISION=`grep "^APP_REVISION" ${FUNCS_DIR}/web/config.py | cut -d"=" -f2 | sed 's/ //g'` + APP_NAME=`grep "^APP_NAME" ${FUNCS_DIR}/web/config.py | cut -d"=" -f2 | sed "s/'//g" | sed 's/^ //'` APP_LONG_VERSION=${APP_RELEASE}.${APP_REVISION} APP_SHORT_VERSION=`echo ${APP_LONG_VERSION} | cut -d . -f1,2` - APP_SUFFIX=`grep "^APP_SUFFIX" web/config.py | cut -d"=" -f2 | sed 's/ //g' | sed "s/'//g"` + APP_SUFFIX=`grep "^APP_SUFFIX" ${FUNCS_DIR}/web/config.py | cut -d"=" -f2 | sed 's/ //g' | sed "s/'//g"` if [ ! -z ${APP_SUFFIX} ]; then APP_LONG_VERSION=${APP_LONG_VERSION}-${APP_SUFFIX} fi @@ -182,7 +183,7 @@ _fixup_imports() { for LIB in $( otool -L ${TODO_OBJ} | \ sed -n 's|^.*[[:space:]]\([^[:space:]]*\.dylib\).*$|\1|p' | \ - egrep -v '^(/usr/lib)|(/System)|@executable_path' \ + egrep -v '^(/usr/lib)|(/System)|@executable_path|@loader_path|/DLC/PIL/' \ ); do # Copy in any required dependencies LIB_BN="$(basename "${LIB}")" ; diff --git a/requirements.txt b/requirements.txt index 06b04f886..256598289 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,3 +42,6 @@ user-agents==2.2.0 pywinpty==1.1.1; sys_platform=="win32" Authlib==0.15.* requests==2.25.* +pyotp==2.* +qrcode==7.* +Pillow==8.3.* diff --git a/web/.eslintignore b/web/.eslintignore index 676a4f03f..eab18762b 100644 --- a/web/.eslintignore +++ b/web/.eslintignore @@ -4,3 +4,4 @@ vendor templates/ templates\ ycache +regression/htmlcov diff --git a/web/config.py b/web/config.py index 2d6b544d4..c0079762a 100644 --- a/web/config.py +++ b/web/config.py @@ -743,6 +743,29 @@ WEBSERVER_AUTO_CREATE_USER = True WEBSERVER_REMOTE_USER = 'REMOTE_USER' +########################################################################## +# Two-factor Authentication Configuration +########################################################################## + +# Set it to True, to enable the two-factor authentication +MFA_ENABLED = True + +# Set it to True, to ask the users to register forcefully for the +# two-authentication methods on logged-in. +MFA_FORCE_REGISTRATION = False + +# pgAdmin supports Two-factor authentication by either sending an one-time code +# to an email, or using the TOTP based application like Google Authenticator. +MFA_SUPPORTED_METHODS = ["email", "authenticator"] + +# NOTE: Please set the 'Mail server settings' to use 'email' as two-factor +# authentication method. + +# Subject for the email verification code +# Default: - Verification Code +# e.g. pgAdmin 4 - Verification Code +MFA_EMAIL_SUBJECT = None + ########################################################################## # PSQL tool settings ########################################################################## diff --git a/web/migrations/script.py.mako b/web/migrations/script.py.mako index 8cda71380..340757c99 100644 --- a/web/migrations/script.py.mako +++ b/web/migrations/script.py.mako @@ -14,8 +14,7 @@ Revises: ${down_revision | comma,n} Create Date: ${create_date} """ -from alembic import op -import sqlalchemy as sa +from pgadmin.model import db ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/web/migrations/versions/15c88f765bc8_.py b/web/migrations/versions/15c88f765bc8_.py new file mode 100644 index 000000000..7887ca9d8 --- /dev/null +++ b/web/migrations/versions/15c88f765bc8_.py @@ -0,0 +1,44 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Update DB to version 31 + +Added a table `user_mfa` for saving the options on MFA for different sources. + +Revision ID: 15c88f765bc8 +Revises: 6650c52670c2 +Create Date: 2021-11-24 17:33:12.533825 + +""" +from pgadmin.model import db + + +# revision identifiers, used by Alembic. +revision = '15c88f765bc8' +down_revision = '6650c52670c2' +branch_labels = None +depends_on = None + + +def upgrade(): + db.engine.execute(""" +CREATE TABLE user_mfa( + user_id INTEGER NOT NULL, + mfa_auth VARCHAR(256) NOT NULL, + options TEXT, + PRIMARY KEY (user_id, mfa_auth), + FOREIGN KEY(user_id) REFERENCES user (id) +) + """) + # ### end Alembic commands ### + + +def downgrade(): + # pgAdmin only upgrades, downgrade not implemented. + pass diff --git a/web/migrations/versions/6650c52670c2_.py b/web/migrations/versions/6650c52670c2_.py index 90cc17d36..4f6f508e1 100644 --- a/web/migrations/versions/6650c52670c2_.py +++ b/web/migrations/versions/6650c52670c2_.py @@ -1,5 +1,13 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## -"""empty message +"""Update DB to version 30 Revision ID: 6650c52670c2 Revises: c465fee44968 diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index e41242ac3..58d4930b4 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -19,7 +19,7 @@ from flask_security.views import _security from flask_security.utils import get_post_logout_redirect, \ get_post_login_redirect, logout_user -from pgadmin import db, User +from pgadmin.model import db, User from pgadmin.utils import PgAdminModule from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP from pgadmin.authenticate.registry import AuthSourceRegistry @@ -27,6 +27,26 @@ from pgadmin.authenticate.registry import AuthSourceRegistry MODULE_NAME = 'authenticate' auth_obj = None +_URL_WITH_NEXT_PARAM = "{0}?next={1}" + + +def get_logout_url() -> str: + """ + Returns the logout url based on the current authentication method. + + Returns: + str: logout url + """ + BROWSER_INDEX = 'browser.index' + if config.SERVER_MODE and\ + session['auth_source_manager']['current_source'] == \ + KERBEROS: + return _URL_WITH_NEXT_PARAM.format(url_for( + 'authenticate.kerberos_logout'), url_for(BROWSER_INDEX)) + + return _URL_WITH_NEXT_PARAM.format( + url_for('security.logout'), url_for(BROWSER_INDEX)) + class AuthenticateModule(PgAdminModule): def get_exposed_url_endpoints(self): diff --git a/web/pgadmin/authenticate/mfa/__init__.py b/web/pgadmin/authenticate/mfa/__init__.py new file mode 100644 index 000000000..51dd12658 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/__init__.py @@ -0,0 +1,110 @@ +############################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################################## +"""Multi-factor Authentication (MFA) implementation""" + +from flask import Blueprint, session, Flask +from flask_babel import gettext as _ + +import config +from .utils import mfa_enabled, segregate_valid_and_invalid_mfa_methods + +from .registry import MultiFactorAuthRegistry +from .views import validate_view, registration_view + + +def __create_blueprint() -> Blueprint: + """ + Geneates the blueprint for 'mfa' endpoint, and also - define the required + endpoints within that blueprint. + + Returns: + Blueprint: MFA blueprint object + """ + blueprint = Blueprint( + "mfa", __name__, url_prefix="/mfa", + static_folder="static", + template_folder="templates" + ) + + blueprint.add_url_rule( + "/validate", "validate", validate_view, methods=("GET", "POST",) + ) + + blueprint.add_url_rule( + "/register", "register", registration_view, methods=("GET", "POST",) + ) + + return blueprint + + +def init_app(app: Flask): + """ + Initialize the flask application for the multi-faction authentication + end-points, when the SERVER_MODE is set to True, and MFA_ENABLED is set to + True in the configuration file. + + Args: + app (Flask): Flask Application object + """ + + if getattr(config, "SERVER_MODE", False) is False and \ + getattr(config, "MFA_ENABLED", False) is False: + return + + MultiFactorAuthRegistry.load_modules(app) + + def exclude_invalid_mfa_auth_methods(): + """ + Exclude the invalid MFA auth methods specified in MFA_SUPPORTED_METHODS + configuration. + """ + + supported_methods = getattr(config, "MFA_SUPPORTED_METHODS", []) + invalid_auth_methods = [] + + supported_methods, invalid_auth_methods = \ + segregate_valid_and_invalid_mfa_methods(supported_methods) + + for auth_method in invalid_auth_methods: + app.logger.warning(_( + "'{}' is not a valid multi-factor authentication method" + ).format(auth_method)) + + config.MFA_SUPPORTED_METHODS = supported_methods + blueprint = __create_blueprint() + + for mfa_method in supported_methods: + mfa = MultiFactorAuthRegistry.get(mfa_method) + mfa.register_url_endpoints(blueprint) + + app.register_blueprint(blueprint) + app.register_logout_hook(blueprint) + + from flask_login import user_logged_out + + @user_logged_out.connect_via(app) + def clear_session_on_login(sender, user): + session['mfa_authenticated'] = False + + def disable_mfa(): + """ + Set MFA_ENABLED configuration to False. + + Also - log a warning message about no valid authentication method found + during initialization. + """ + if getattr(config, 'MFA_ENABLED', False) is True and \ + getattr(config, 'SERVER_MODE', False) is True: + app.logger.warning(_( + "No valid multi-factor authentication found, hence - " + "disabling it." + )) + config.MFA_ENABLED = False + + mfa_enabled(exclude_invalid_mfa_auth_methods, disable_mfa) diff --git a/web/pgadmin/authenticate/mfa/authenticator.py b/web/pgadmin/authenticate/mfa/authenticator.py new file mode 100644 index 000000000..900166ec4 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/authenticator.py @@ -0,0 +1,222 @@ +############################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################################## +"""Multi-factor Authentication implementation for Time-based One-Time Password +(TOTP) applications""" + +import base64 +from io import BytesIO +from typing import Union + +from flask import url_for, session, flash +from flask_babel import gettext as _ +from flask_login import current_user +import pyotp +import qrcode + +import config +from pgadmin.model import UserMFA + +from .registry import BaseMFAuth +from .utils import ValidationException, fetch_auth_option, mfa_add + + +_TOTP_AUTH_METHOD = "authenticator" +_TOTP_AUTHENTICATOR = _("Authenticator App") + + +class TOTPAuthenticator(BaseMFAuth): + """ + Authenction class for TOTP based authentication. + + Base Class: BaseMFAuth + """ + + @classmethod + def __create_topt_for_currentuser(cls) -> pyotp.TOTP: + """ + Create the TOPT object using the secret stored for the current user in + the configuration database. + + Assumption: Configuration database is not modified by anybody manually, + and removed the secrete for the current user. + + Raises: + ValidationException: Raises when user is not registered for this + authenction method. + + Returns: + pyotp.TOTP: TOTP object for the current user (if registered) + """ + options, found = fetch_auth_option(_TOTP_AUTH_METHOD) + + if found is False: + raise ValidationException(_( + "User has not registered the Time-based One-Time Password " + "(TOTP) Authenticator for authentication." + )) + + if options is None or options == '': + raise ValidationException(_( + "User does not have valid HASH to generate the OTP." + )) + + return pyotp.TOTP(options) + + @property + def name(self) -> str: + """ + Name of the authetication method for internal presentation. + + Returns: + str: Short name for this authentication method + """ + return _TOTP_AUTH_METHOD + + @property + def label(self) -> str: + """ + Label for the UI for this authentication method. + + Returns: + str: User presentable string for this auth method + """ + return _(_TOTP_AUTHENTICATOR) + + @property + def icon(self) -> str: + """ + Property for the icon url string for this auth method, to be used on + the authentication or registration page. + + Returns: + str: url for the icon representation for this auth method + """ + return url_for("mfa.static", filename="images/totp_lock.svg") + + def validate(self, **kwargs): + """ + Validate the code sent using the HTTP request. + + Raises: + ValidationException: Raises when code is not valid + """ + code = kwargs.get('code', None) + totp = TOTPAuthenticator.__create_topt_for_currentuser() + + if totp.verify(code) is False: + raise ValidationException("Invalid Code") + + def validation_view(self) -> str: + """ + Generate the portion of the view to render on the authentication page + + Returns: + str: Authentication view as a string + """ + return ( + "
{auth_description}
" + "
" + " " + "
" + ).format( + auth_description=_( + "Enter the code shown in your authenticator application for " + "TOTP (Time-based One-Time Password)" + ), + otp_placeholder=_("Enter code"), + ) + + def _registration_view(self) -> str: + """ + Internal function to generate a view for the registration page. + + View will contain the QRCode image for the TOTP based authenticator + applications to scan. + + Returns: + str: Registration view with QRcode for TOTP based applications + """ + + option = session.pop('mfa_authenticator_opt', None) + if option is None: + option = pyotp.random_base32() + session['mfa_authenticator_opt'] = option + totp = pyotp.TOTP(option) + + uri = totp.provisioning_uri( + current_user.username, issuer_name=getattr( + config, "APP_NAME", "pgAdmin 4" + ) + ) + + img = qrcode.make(uri) + buffered = BytesIO() + img.save(buffered, format="JPEG") + img_base64 = base64.b64encode(buffered.getvalue()) + + return "".join([ + "
{auth_title}
", + "", + "", + "{qrcode_alt_text}", + "
{auth_description}
", + "
", + "", + "
", + ]).format( + auth_title=_(_TOTP_AUTHENTICATOR), + auth_method=_TOTP_AUTH_METHOD, + image=img_base64.decode("utf-8"), + qrcode_alt_text=_("TOTP Authenticator QRCode"), + auth_description=_( + "Scan the QR code and the enter the code from the " + "TOTP Authenticator application" + ), otp_placeholder=_("Enter code") + ) + + def registration_view(self, form_data) -> Union[str, None]: + """ + Returns the registration view for this authentication method. + + It is also responsible for validating the code during the registration. + + Args: + form_data (dict): Form data as a dictionary sent from the + registration page for rendering or validation of + the code. + + Returns: + str: Registration view for the 'authenticator' method if it is not + a request for the validation of the code or the code sent is + not a valid TOTP code, otherwise - it will return None. + """ + + if 'VALIDATE' not in form_data: + return self._registration_view() + + code = form_data.get('code', None) + authenticator_opt = session.get('mfa_authenticator_opt', None) + if authenticator_opt is None or \ + pyotp.TOTP(authenticator_opt).verify(code) is False: + flash(_("Failed to validate the code"), "danger") + return self._registration_view() + + mfa_add(_TOTP_AUTH_METHOD, authenticator_opt) + flash(_( + "TOTP Authenticator registered successfully for authentication." + ), "success") + session.pop('mfa_authenticator_opt', None) + + return None diff --git a/web/pgadmin/authenticate/mfa/email.py b/web/pgadmin/authenticate/mfa/email.py new file mode 100644 index 000000000..b0a2e2709 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/email.py @@ -0,0 +1,310 @@ +############################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################################## +"""Multi-factor Authentication implementation by sending OTP through email""" + +from flask import url_for, session, Response, render_template, current_app, \ + flash +from flask_babel import gettext as _ +from flask_login import current_user +from flask_security import send_mail + +import config +from pgadmin.utils.csrf import pgCSRFProtect +from .registry import BaseMFAuth +from .utils import ValidationException, mfa_add, fetch_auth_option + + +def __generate_otp() -> str: + """ + Generate a six-digits one-time-password (OTP) for the current user. + + Returns: + str: A six-digits OTP for the current user + """ + import time + import base64 + import codecs + import random + + code = codecs.encode("{}{}{}".format( + time.time(), current_user.username, random.randint(1000, 9999) + ).encode(), "hex") + + res = 0 + idx = 0 + + while idx < len(code): + res += int((code[idx:idx + 6]).decode('utf-8'), base=16) + res %= 1000000 + idx += 5 + + return str(res).zfill(6) + + +def _send_code_to_email(_email: str = None) -> (bool, int, str): + """ + Send the code to the email address, provided in the argument or to the + email address of the current user, provided during the registration. + + Args: + _email (str, optional): Email Address, where to send the OTP code. + Defaults to None. + + Returns: + (bool, int, str): Returns a set as (failed?, HTTP Code, message string) + If 'failed?' is True, message contains the error + message for the user, else it contains the success + message for the user to consume. + """ + + if not current_user.is_authenticated: + return False, 401, _("Not accessible") + + if _email is None: + _email = getattr(current_user, 'email', None) + + if _email is None: + return False, 401, _("No email address is available.") + + try: + session["mfa_email_code"] = __generate_otp() + subject = getattr(config, 'MFA_EMAIL_SUBJECT', None) + + if subject is None: + subject = _("{} - Verification Code").format(config.APP_NAME) + + send_mail( + subject, + _email, + "send_email_otp", + user=current_user, + code=session["mfa_email_code"] + ) + except OSError as ose: + current_app.logger.exception(ose) + return False, 503, _("Failed to send the code to email.") + \ + "\n" + str(ose) + + message = _( + "A verification code was sent to {}. Check your email and enter " + "the code." + ).format(_mask_email(_email)) + + return True, 200, message + + +def _mask_email(_email: str) -> str: + """ + + Args: + _email (str): Email address to be masked + + Returns: + str: Masked email address + """ + import re + email_split = re.split('@', _email) + username, domain = email_split + domain_front, *domain_back_list = re.split('[.]', domain) + users = re.split('[.]', username) + + def _mask_except_first_char(_str: str) -> str: + """ + Mask all characters except first character of the input string. + Args: + _str (str): Input string to be masked + + Returns: + str: Masked string + """ + return _str[0] + '*' * (len(_str) - 1) + + return '.'.join([_mask_except_first_char(user) for user in users]) + \ + '@' + _mask_except_first_char(domain_front) + '.' + \ + '.'.join(domain_back_list) + + +def send_email_code() -> Response: + """ + Send the code to the users' email address, stored during the registration. + + Raises: + ValidationException: Raise this exception when user is not registered + for this authentication method. + + Returns: + Flask.Response: Response containing the HTML portion after sending the + code to the registered email address of the user. + """ + + options, found = fetch_auth_option(EMAIL_AUTH_METHOD) + + if found is False: + raise ValidationException(_( + "User has not registered for email authentication" + )) + + success, http_code, message = _send_code_to_email(options) + + if success is False: + return Response(message, http_code, mimetype='text/html') + + return Response(render_template( + "mfa/email_code_sent.html", _=_, + message=message, + ), http_code, mimetype='text/html') + + +@pgCSRFProtect.exempt +def javascript() -> Response: + """ + Returns the javascript code for the email authentication method. + + Returns: + Flask.Response: Response object conataining the javscript code for the + email auth method. + """ + if not current_user.is_authenticated: + return Response(_("Not accessible"), 401, mimetype="text/plain") + + return Response(render_template( + "mfa/email.js", _=_, url_for=url_for, + ), 200, mimetype="text/javascript") + + +EMAIL_AUTH_METHOD = 'email' + + +def email_authentication_label(): + return _('Email Authentication') + + +class EmailAuthentication(BaseMFAuth): + + @property + def name(self): + return EMAIL_AUTH_METHOD + + @property + def label(self): + return email_authentication_label() + + def validate(self, **kwargs): + code = kwargs.get('code', None) + email_otp = session.get("mfa_email_code", None) + if code is not None and email_otp is not None and code == email_otp: + session.pop("mfa_email_code") + return + raise ValidationException("Invalid code") + + def validation_view(self): + session.pop("mfa_email_code", None) + return render_template( + "mfa/email_view.html", _=_ + ) + + def _registration_view(self): + email = getattr(current_user, 'email', '') + return "\n".join([ + "
{label}
", + "", + "", + "
{description}
", + "
", + " ", + "
", + "", + ]).format( + label=email_authentication_label(), + auth_method=EMAIL_AUTH_METHOD, + description=_("Enter the email address to send a code"), + email_address_placeholder=_("Email address"), + email_address=email, + note_label=_("Note"), + note=_( + "This email address will only be used by the authentication " + "purpose. It will not update the user's email address." + ), + ) + + def _registration_view_after_code_sent(self, _form_data): + + session['mfa_email_id'] = _form_data.get('send_to', None) + success, http_code, message = _send_code_to_email( + session['mfa_email_id'] + ) + + if success is False: + flash(message, 'danger') + return None + + return "\n".join([ + "
{label}
", + "", + "", + "
{message}
", + "
", + " ", + "
", + ]).format( + label=email_authentication_label(), + auth_method=EMAIL_AUTH_METHOD, + message=message, + otp_placeholder=_("Enter code here") + ) + + def registration_view(self, _form_data): + + if 'validate' in _form_data: + if _form_data['validate'] == 'send_code': + return self._registration_view_after_code_sent(_form_data) + + code = _form_data.get('code', 'unknown') + + if code is not None and \ + code == session.get("mfa_email_code", None) and \ + session.get("mfa_email_id", None) is not None: + mfa_add(EMAIL_AUTH_METHOD, session['mfa_email_id']) + + flash(_( + "Email Authentication registered successfully." + ), "success") + + session.pop('mfa_email_code', None) + + return None + + flash(_('Invalid code'), 'danger') + + return self._registration_view() + + def register_url_endpoints(self, blueprint): + blueprint.add_url_rule( + "/send_email_code", "send_email_code", send_email_code, + methods=("POST", ) + ) + blueprint.add_url_rule( + "/email.js", "email_js", javascript, methods=("GET", ) + ) + + @property + def icon(self): + return url_for("mfa.static", filename="images/email_lock.svg") + + @property + def validate_script(self): + return url_for("mfa.email_js") diff --git a/web/pgadmin/authenticate/mfa/registry.py b/web/pgadmin/authenticate/mfa/registry.py new file mode 100644 index 000000000..1469551fb --- /dev/null +++ b/web/pgadmin/authenticate/mfa/registry.py @@ -0,0 +1,167 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""External 2FA Authentication Registry.""" +from abc import abstractmethod, abstractproperty +import six +from typing import Union + +import flask + +from pgadmin.utils.dynamic_registry import create_registry_metaclass + + +""" +class: MultiFactorAuthRegistry + +An registry factory for the multi-factor authentication methods. +""" +MultiFactorAuthRegistry = create_registry_metaclass( + 'MultiFactorAuthRegistry', __package__, decorate_as_module=True +) + + +@six.add_metaclass(MultiFactorAuthRegistry) +class BaseMFAuth(): + """ + Base Multi-Factor Authentication (MFA) class + + A Class implements this class will be registered with + the registry class 'MultiFactorAuthRegistry', and it will be automatically + available as a MFA method. + """ + + @abstractproperty + def name(self) -> str: + """ + Represents the short name for the authentiation method. It can be used + in the MFA_SUPPORTED_METHODS parameter in the configuration as a + supported authentication method. + + Returns: + str: Short name for this authentication method + + NOTE: Name must not contain special characters + """ + pass + + @abstractproperty + def label(self) -> str: + """ + Represents the user visible name for the authentiation method. It will + be visible on the authentication page and registration page. + + Returns: + str: Value for the UI for the authentication method + """ + pass + + @property + def icon(self) -> str: + """ + A url for the icon for the authentication method. + + Returns: + str: Value for the UI for the authentication method + """ + return "" + + @property + def validate_script(self) -> Union[str, None]: + """ + A url route for the javscript required for the auth method. + + Override this method for the auth methods, when it required a + javascript on the authentication page. + + Returns: + Union[str, None]: Url for the auth method or None + """ + return None + + @abstractmethod + def validate(self, **kwargs) -> str: + """ + Validate the code/password sent using the HTTP request during the + authentication process. + + If the validation is not done successfully for some reason, it must + raise a ValidationException exception. + + Parameters: + kwargs: data sent during the authentication process + + Raises: + ValidationException: Raises when code/otp is not valid + """ + pass + + @abstractmethod + def validation_view(self) -> str: + """ + Authenction route (view) for the auth method. + """ + pass + + @abstractmethod + def registration_view(self, form_data) -> str: + """ + Registration View for the auth method. + + Must override this for rendering the registration page for the auth + method. + + Args: + form_data (dict): Form data sent from the registration page. + """ + pass + + def register_url_endpoints(self, blueprint: flask.Blueprint) -> None: + """ + Register the URL end-points for the auth method (special case). + + Args: + blueprint (flask.Blueprint): MFA blueprint for registering the + end-point for the method + + + NOTE: Override this method only when there is special need to expose + an url end-point for the auth method. + """ + pass + + def to_dict(self) -> dict: + """ + A diction representation for the auth method. + + Returns: + dict (id, label, icon): Diction representation for an auth method. + """ + return { + "id": self.name, + "label": self.label, + "icon": self.icon, + } + + def validation_view_dict(self, selected_mfa: str) -> dict: + """ + A diction representation for the auth method to be used on the + registration page. + + Returns: + dict: Diction representation for an auth method to be used on the + regisration page. + """ + res = self.to_dict() + + res['view'] = self.validation_view() + res['selected'] = selected_mfa == self.name + res['script'] = self.validate_script + + return res diff --git a/web/pgadmin/authenticate/mfa/static/images/email_lock.svg b/web/pgadmin/authenticate/mfa/static/images/email_lock.svg new file mode 100644 index 000000000..385b99b60 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/static/images/email_lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/pgadmin/authenticate/mfa/static/images/totp_lock.svg b/web/pgadmin/authenticate/mfa/static/images/totp_lock.svg new file mode 100644 index 000000000..fb3d685a9 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/static/images/totp_lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email.js b/web/pgadmin/authenticate/mfa/templates/mfa/email.js new file mode 100644 index 000000000..e6fe6f57b --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/mfa/email.js @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +var mfa_form_elem = document.getElementById('mfa_form'); + +if (mfa_form_elem) + mfa_form_elem.setAttribute('class', ''); + +function sendCodeToEmail(data, _json, _callback) { + const URL = '{{ url_for('mfa.send_email_code') }}'; + let accept = 'text/html; charset=utf-8;'; + + var btn_send_code_elem = document.getElementById('btn_send_code'); + if (btn_send_code_elem) btn_send_code_elem.disabled = true; + + if (!data) { + data = {'code': ''}; + } + + if (_json) { + accept = 'application/json; charset=utf-8;'; + } + + clear_error(); + + fetch(URL, { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + headers: { + 'Accept': accept, + 'Content-Type': 'application/json; charset=utf-8;', + '{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}': '{{ csrf_token() }}' + }, + redirect: 'follow', + body: JSON.stringify(data) + }).then((resp) => { + if (_callback) { + setTimeout(() => (_callback(resp)), 1); + return null; + } + if (!resp.ok) { + var btn_send_code_elem = document.getElementById('btn_send_code'); + if (btn_send_code_elem) btn_send_code_elem.disabled = true; + resp.text().then(msg => render_error(msg)); + + return; + } + if (_json) return resp.json(); + return resp.text(); + }).then((string) => { + if (!string) + return; + document.getElementById("mfa_email_auth").innerHTML = string; + document.getElementById("mfa_form").classList = ["show_validate_btn"]; + setTimeout(() => { + document.getElementById("showme").classList = []; + }, 20000); + }); +} diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html b/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html new file mode 100644 index 000000000..0325ac9e5 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html @@ -0,0 +1,19 @@ +
diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html b/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html new file mode 100644 index 000000000..3ecf635fb --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html @@ -0,0 +1,7 @@ +
+
{{ _("Verify with Email Authentication") }}
+
+ +
+
diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/register.html b/web/pgadmin/authenticate/mfa/templates/mfa/register.html new file mode 100644 index 000000000..7b5173e97 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/mfa/register.html @@ -0,0 +1,78 @@ +{% set auth_page = true %} +{% extends "security/panel.html" %} +{% block panel_image %} +
+ {{ _('Registration') }} +
+{% endblock %} +{% block panel_title %}{{ _('Authentication registration') }}{% endblock %} +{% block panel_body %} + + +
+
+{% if mfa_view is not defined or mfa_view is none %} +
+ {% for mfa in mfa_list %} +
+ +
+ {% endfor %} +
+ {% if next_url != 'internal' %} +
+ +
+ {% endif %} +{% else %} +
+ {{ mfa_view | safe }} +
+
+ + +
+{% endif %} + +
+
+{% else %} +
+
+
+
+ +
+ {{ error_message }}
+
+
+
+
+{% endif %} +{% endblock %} diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/validate.html b/web/pgadmin/authenticate/mfa/templates/mfa/validate.html new file mode 100644 index 000000000..acb07b004 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/mfa/validate.html @@ -0,0 +1,121 @@ +{% extends "security/panel.html" %} +{% block panel_image %} +
+ {{ _('Authentication') }} +
+{% endblock %} +{% block panel_title %}{{ _('Authentication') }}{% endblock %} +{% block panel_body %} + + +
+
+
+
+
+ +
+ +
+
+
+
+ +
+ + +{% endblock %} diff --git a/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.html b/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.html new file mode 100644 index 000000000..f9bf492d2 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.html @@ -0,0 +1,2 @@ +Please use the following code for authentication. +{{ code }} diff --git a/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.txt b/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.txt new file mode 100644 index 000000000..f9bf492d2 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.txt @@ -0,0 +1,2 @@ +Please use the following code for authentication. +{{ code }} diff --git a/web/pgadmin/authenticate/mfa/tests/test_config.py b/web/pgadmin/authenticate/mfa/tests/test_config.py new file mode 100644 index 000000000..782dc923b --- /dev/null +++ b/web/pgadmin/authenticate/mfa/tests/test_config.py @@ -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", + ), + ), +] diff --git a/web/pgadmin/authenticate/mfa/tests/test_mfa.py b/web/pgadmin/authenticate/mfa/tests/test_mfa.py new file mode 100644 index 000000000..01264f611 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/tests/test_mfa.py @@ -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) diff --git a/web/pgadmin/authenticate/mfa/tests/test_mfa_view.py b/web/pgadmin/authenticate/mfa/tests/test_mfa_view.py new file mode 100644 index 000000000..12590e10b --- /dev/null +++ b/web/pgadmin/authenticate/mfa/tests/test_mfa_view.py @@ -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), + ), +] diff --git a/web/pgadmin/authenticate/mfa/tests/test_user_execution.py b/web/pgadmin/authenticate/mfa/tests/test_user_execution.py new file mode 100644 index 000000000..fbf3e8c61 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/tests/test_user_execution.py @@ -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), + ), +] diff --git a/web/pgadmin/authenticate/mfa/tests/utils.py b/web/pgadmin/authenticate/mfa/tests/utils.py new file mode 100644 index 000000000..1bd15ebf0 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/tests/utils.py @@ -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("logged in") + + 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 diff --git a/web/pgadmin/authenticate/mfa/utils.py b/web/pgadmin/authenticate/mfa/utils.py new file mode 100644 index 000000000..f3e8f9b53 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/utils.py @@ -0,0 +1,408 @@ +############################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################################## +"""Multi-factor Authentication (MFA) utility functions""" + +from collections.abc import Callable +from functools import wraps + +from flask import url_for, session, request, redirect +from flask_login.utils import login_url +from flask_security import current_user + +import config +from pgadmin.model import UserMFA, db +from .registry import MultiFactorAuthRegistry + + +class ValidationException(Exception): + """ + class: ValidationException + Base class: Exception + + An exception class for raising validation issue. + """ + pass + + +def segregate_valid_and_invalid_mfa_methods( + mfa_supported_methods: list +) -> (list, list): + """ + Segregate the valid and invalid authentication methods from the given + methods. + + Args: + mfa_supported_methods (list): List of auth methods + + Returns: + list, list: Set of valid & invalid auth methods + """ + + invalid_auth_methods = [] + valid_auth_methods = [] + + for mfa in mfa_supported_methods: + + # Put invalid MFA method in separate list + if mfa not in MultiFactorAuthRegistry._registry: + if mfa not in invalid_auth_methods: + invalid_auth_methods.append(mfa) + continue + + # Exclude the duplicate entries + if mfa in valid_auth_methods: + continue + + valid_auth_methods.append(mfa) + + return valid_auth_methods, invalid_auth_methods + + +def mfa_suppored_methods() -> dict: + """ + Returns the dictionary containing information on all supported methods with + information about whether they're registered for the current user, or not. + + It returns information in this format: + { + : { + "mfa": , + "registered": True|False + }, + ... + } + + Returns: + dict: List of all supported MFA methods with the flag for the + registered with the current user or not. + """ + supported_mfa_auth_methods = dict() + + for auth_method in config.MFA_SUPPORTED_METHODS: + registry = MultiFactorAuthRegistry.get(auth_method) + supported_mfa_auth_methods[registry.name] = { + "mfa": registry, "registered": False + } + + auths = UserMFA.query.filter_by(user_id=current_user.id).all() + + for auth in auths: + if auth.mfa_auth in supported_mfa_auth_methods: + supported_mfa_auth_methods[auth.mfa_auth]['registered'] = True + + return supported_mfa_auth_methods + + +def user_supported_mfa_methods(): + """ + Returns the dict for the authentication methods, registered for the + current user, among the list of supported. + + Returns: + dict: dict for the auth methods + """ + auths = UserMFA.query.filter_by(user_id=current_user.id).all() + res = dict() + supported_mfa_auth_methods = dict() + + if len(auths) > 0: + for auth_method in config.MFA_SUPPORTED_METHODS: + registry = MultiFactorAuthRegistry.get(auth_method) + supported_mfa_auth_methods[registry.name] = registry + + for auth in auths: + if auth.mfa_auth in supported_mfa_auth_methods: + res[auth.mfa_auth] = \ + supported_mfa_auth_methods[auth.mfa_auth] + + return res + + +def is_mfa_session_authenticated() -> bool: + """ + Checks if this session is authenticated, or not. + + Returns: + bool: Is this session authenticated? + """ + return session.get('mfa_authenticated', False) is True + + +def mfa_enabled(execute_if_enabled, execute_if_disabled) -> None: + """ + A ternary method to enable calling either of the methods based on the + configuration for the MFA. + + When MFA is enabled and has a valid supported auth methods, + 'execute_if_enabled' method is executed, otherwise - + 'execute_if_disabled' method is executed. + + Args: + execute_if_enabled (Callable[[], None]): Method to executed when MFA + is enabled. + execute_if_disabled (Callable[[], None]): Method to be executed when + MFA is disabled. + + Returns: + None: Expecting the methods to return None as it will not be consumed. + + NOTE: Removed the typing anotation as it was giving errors. + """ + + is_server_mode = getattr(config, 'SERVER_MODE', False) + enabled = getattr(config, "MFA_ENABLED", False) + supported_methods = getattr(config, "MFA_SUPPORTED_METHODS", []) + + if is_server_mode is True and enabled is True and \ + type(supported_methods) == list: + supported_methods, _ = segregate_valid_and_invalid_mfa_methods( + supported_methods + ) + + if len(supported_methods) > 0: + return execute_if_enabled() + + return execute_if_disabled() + + +def mfa_user_force_registration_required(register, not_register) -> None: + """ + A ternary method to cenable calling either of the methods based on the + condition force registration is required. + + When force registration is enabled, and the current user has not registered + for any of the supported authentication method, then the 'register' method + is executed, otherwise - 'not_register' method is executed. + + Args: + register (Callable[[], None]) : Method to be executed when for + registration required and user has + not registered for any auth method. + not_register (Callable[[], None]): Method to be executed otherwise. + + Returns: + None: Expecting the methods to return None as it will not be consumed. + """ + return register() \ + if getattr(config, "MFA_FORCE_REGISTRATION", False) is True else \ + not_register() + + +def mfa_user_registered(registered, not_registered) -> None: + """ + A ternary method to enable calling either of the methods based on the + condition - if the user is registed for any of the auth methods. + + When current user is registered for any of the supported auth method, then + the 'registered' method is executed, otherwise - 'not_registered' method is + executed. + + Args: + registered (Callable[[], None]) : Method to be executed when + registered. + not_registered (Callable[[], None]): Method to be executed when not + registered + + Returns: + None: Expecting the methods to return None as it will not be consumed. + + NOTE: Removed the typing anotation as it was giving errors. + """ + + return registered() if len(user_supported_mfa_methods()) > 0 else \ + not_registered() + + +def mfa_session_authenticated(authenticated, unauthenticated): + """ + A ternary method to enable calling either of the methods based on the + condition - if the user has already authenticated, or not. + + When current user is already authenticated, then 'authenticated' method is + executed, otherwise - 'unauthenticated' method is executed. + + Args: + authenticated (Callable[[], None]) : Method to be executed when + user is authenticated. + unauthenticated (Callable[[], None]): Method to be executed when the + user is not passed the + authentication. + + Returns: + None: Expecting the methods to return None as it will not be consumed. + + NOTE: Removed the typing anotation as it was giving errors. + """ + return authenticated() if session.get('mfa_authenticated', False) is True \ + else unauthenticated() + + +def mfa_required(wrapped): + """ + A decorator do decide the next course of action when a page is being + opened, it will open the appropriate page in case the 2FA is not passed. + + Function executed + | + Check for MFA Enabled? --------+ + | | + | No | + | | Yes + Run the wrapped function [END] | + | + Is user has registered for at least one MFA method? -+ + | | + | No | + | | + Is force registration required? -+ | + | | | Yes + | No | | + | | Yes | + Run the wrapped function [END] | | + | | + Open Registration page [END] | + | + Open the authentication page [END] + + Args: + func(Callable[..]): Method to be called if authentcation is passed + """ + + def get_next_url(): + next_url = request.url + registration_url = url_for('mfa.register') + + if next_url.startswith(registration_url): + return url('browser.index') + + return next_url + + def redirect_to_mfa_validate_url(): + return redirect(login_url("mfa.validate", next_url=get_next_url())) + + def redirect_to_mfa_registration(): + return redirect(login_url("mfa.register", next_url=get_next_url())) + + @wraps(wrapped) + def inner(*args, **kwargs): + + def execute_func(): + session['mfa_authenticated'] = True + return wrapped(*args, **kwargs) + + def if_else_func(_func, first, second): + def if_else_func_inner(): + return _func(first, second) + return if_else_func_inner + + return mfa_enabled( + if_else_func( + mfa_session_authenticated, + execute_func, + if_else_func( + mfa_user_registered, + redirect_to_mfa_validate_url, + if_else_func( + mfa_user_force_registration_required, + redirect_to_mfa_registration, + execute_func + ) + ) + ), + execute_func + ) + + return inner + + +def is_mfa_enabled() -> bool: + """ + Returns True if MFA is enabled otherwise False + + Returns: + bool: Is MFA Enabled? + """ + return mfa_enabled(lambda: True, lambda: False) + + +def mfa_delete(auth_name: str) -> bool: + """ + A utility function to delete the auth method for the current user from the + configuration database. + + Args: + auth_name (str): Name of the argument + + Returns: + bool: True if auth method was registered for the current user, and + delete successfully, otherwise - False + """ + auth = UserMFA.query.filter_by( + user_id=current_user.id, mfa_auth=auth_name + ) + + if auth.count() != 0: + auth.delete() + db.session.commit() + + return True + + return False + + +def mfa_add(auth_name: str, options: str) -> None: + """ + A utility funtion to add/update the auth method in the configuration + database for the current user with the method specific options. + + e.g. email-address for 'email' method, and 'secret' for the 'authenticator' + + Args: + auth_name (str): Name of the auth method + options (str) : A data options specific to the auth method + """ + auth = UserMFA.query.filter_by( + user_id=current_user.id, mfa_auth=auth_name + ).first() + + if auth is None: + auth = UserMFA( + user_id=current_user.id, + mfa_auth=auth_name, + options=options + ) + db.session.add(auth) + + # We will override the existing options + auth.options = options + + db.session.commit() + + +def fetch_auth_option(auth_name: str) -> (str, bool): + """ + A utility function to fetch the extra data, stored as options, for the + given auth method for the current user. + + Returns a set as (data, Auth method registered?) + + Args: + auth_name (str): Name of the auth method + + Returns: + (str, bool): (data, has current user registered for the auth method?) + """ + auth = UserMFA.query.filter_by( + user_id=current_user.id, mfa_auth=auth_name + ).first() + + if auth is None: + return None, False + + return auth.options, True diff --git a/web/pgadmin/authenticate/mfa/views.py b/web/pgadmin/authenticate/mfa/views.py new file mode 100644 index 000000000..c017a4c27 --- /dev/null +++ b/web/pgadmin/authenticate/mfa/views.py @@ -0,0 +1,346 @@ +############################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################################## +"""Multi-factor Authentication (MFA) views""" + +import base64 +from typing import Union + +from flask import Response, render_template, request, flash, \ + current_app, url_for, redirect, session +from flask_babel import gettext as _ +from flask_login import current_user, login_required +from flask_login.utils import login_url + +from pgadmin.utils.csrf import pgCSRFProtect +from pgadmin.utils.ajax import bad_request +from .utils import user_supported_mfa_methods, mfa_user_registered, \ + mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \ + is_mfa_session_authenticated + + +_INDEX_URL = "browser.index" +_NO_CACHE_HEADERS = dict({ + "Cache-Control": "no-cache, no-store, must-revalidate, public, max-age=0", + "Pragma": "no-cache", + "Expires": "0", +}) + + +def __handle_mfa_validation_request( + mfa_method: str, user_mfa_auths: dict, form_data: dict +) -> None: + """ + An internal utlity function to execute mfa.validate(...) method in case, it + matched the following conditions: + 1. Method specified is a valid and in the supported methods list. + 2. User has registered for this auth method. + + Otherwise, raise an exception with appropriate error message. + + Args: + mfa_method (str) : Name of the authentication method + user_mfa_auths (dict): List of the user supported authentication method + form_data (dict) : Form data in the request + + Raises: + ValidationException: Raise the exception when user is not registered + for the given method, or not a valid MFA method. + """ + + if mfa_method is None: + raise ValidationException(_("No authentication method provided.")) + + mfa_auth = user_mfa_auths.get(mfa_method, None) + + if mfa_auth is None: + raise ValidationException(_( + "No user supported authentication method provided" + )) + + mfa_auth.validate(**form_data) + + +@pgCSRFProtect.exempt +@login_required +def validate_view() -> Response: + """ + An end-point to render the authentication view. + + It supports two HTTP methods: + 1. GET : Generate the view listing all the supported auth methods. + 2. POST: Validate the code/OTP, or whatever data the selected auth method + supports. + + Returns: + Response: Redirect to 'next' url in case authentication validate, + otherwise - a view with listing down all the supported auth + methods, and it's supporting views. + """ + + # Load at runtime to avoid circular dependency + from pgadmin.authenticate import get_logout_url + + next_url = request.args.get("next", None) + + if next_url is None or next_url == url_for('mfa.register') or \ + next_url == url_for('mfa.validate'): + next_url = url_for(_INDEX_URL) + + if session.get('mfa_authenticated', False) is True: + return redirect(next_url) + + return_code = 200 + mfa_method = None + user_mfa_auths = user_supported_mfa_methods() + + if request.method == 'POST': + try: + form_data = {key: request.form[key] for key in request.form} + next_url = form_data.pop('next', url_for(_INDEX_URL)) + mfa_method = form_data.pop('mfa_method', None) + + __handle_mfa_validation_request( + mfa_method, user_mfa_auths, form_data + ) + + session['mfa_authenticated'] = True + + return redirect(next_url) + + except ValidationException as ve: + current_app.logger.warning(( + "MFA validation failed for the user '{}' with an error: " + "{}" + ).format(current_user.username, str(ve))) + flash(str(ve), "danger") + return_code = 401 + except Exception as ex: + current_app.logger.exception(ex) + flash(str(ex), "danger") + return_code = 500 + + mfa_views = { + key: user_mfa_auths[key].validation_view_dict(mfa_method) + for key in user_mfa_auths + } + + if mfa_method is None and len(mfa_views) > 0: + list(mfa_views.items())[0][1]['selected'] = True + + return Response(render_template( + "mfa/validate.html", _=_, views=mfa_views, base64=base64, + logout_url=get_logout_url() + ), return_code, headers=_NO_CACHE_HEADERS, mimetype="text/html") + + +def _mfa_registration_view( + supported_mfa: dict, form_data: dict +) -> Union[str, None]: + """ + An internal utility function to generate the registration view, or + unregister for the given MFA object (passed as a dict). + + It will call 'registration_view' function, specific for the MFA method, + only if User has clicked on 'Setup' button on the registration page, and + current user is not already registered for the Auth method. + + If the user has not clicked on the 'Setup' button, we assume that he has + clicked on the 'Delete' button for a specific auth method. + + Args: + supported_mfa (dict): [description] + form_data (dict): [description] + + Returns: + Union[str, None]: When registration for the Auth method is completed, + it could return None, otherwise view for the + registration view. + """ + mfa = supported_mfa['mfa'] + + if form_data[mfa.name] == 'SETUP': + if supported_mfa['registered'] is True: + flash(_("'{}' is already registerd'").format(mfa.label), "success") + return None + + return mfa.registration_view(form_data) + + if mfa_delete(mfa.name) is True: + flash(_( + "'{}' unregistered from the authentication list." + ).format(mfa.label), "success") + + return None + + flash(_( + "'{}' is not found in the authentication list." + ).format(mfa.label), "warning") + + return None + + +def _registration_view_or_deregister( + _auth_list: dict +) -> Union[str, bool, None]: + """ + An internal utility function to parse the request, and act upon it: + 1. Find the auth method in the request, and call the + '_mfa_registration_view' internal utility function for the same, and + return the result of it. + + It could return a registration view as a string, or None (on + deregistering). + + Args: + _auth_list (dict): List of all supported methods with a flag for the + current user registration. + + Returns: + Union[str, bool, None]: When no valid request found, it will return + False, otherwise the response of the + '_mfa_registration_view(...)' method call. + """ + + for key in _auth_list: + if key in request.form: + return _mfa_registration_view( + _auth_list[key], request.form + ) + + return False + + +def __handle_registration_view_for_post_method( + _next_url: str, _mfa_auths: dict +) -> (Union[str, None], Union[Response, None], Union[dict, None]): + """ + An internal utility function to handle the POST method for the registration + view. It will pass on the request data to the appropriate Auth method, and + may generate further registration view. When registration is completed, it + will redirect to the 'next_url' in case the registration page is not opened + from the internal dialog through menu, which can be identified by the + 'next_url' value is equal to 'internal'. + + Args: + _next_url (str) : Redirect to which url, when clicked on the + 'continue' button on the registration page. + _mfa_auths (dict): A dict object returned by the method - + 'mfa_suppored_methods'. + + Returns: + (Union[str, None], Union[Response, None], Union[dict, None]): + Possibilities: + 1. Returns (None, redirect response to 'next' url, None) in case there + is not valid 'auth' method found in the request. + 2. Returns (None, Registration view as Response, None) in case when + valid method found, and it has returned a view to render. + 3. Otherwise - Returns the set as + (updated 'next' url, None, updated Auth method list) + """ + + next_url = request.form.get("next", None) + + if next_url is None or next_url == url_for('mfa.validate'): + next_url = url_for(_INDEX_URL) + + if request.form.get('cancel', None) is None: + view = _registration_view_or_deregister(_mfa_auths) + + if view is False: + if next_url != 'internal': + return None, redirect(next_url), None + flash(_("Please close the dialog."), "info") + + if view is not None: + return None, Response( + render_template( + "mfa/register.html", _=_, + mfa_list=list(), mfa_view=view, + next_url=next_url, + error_message=None + ), 200, + headers=_NO_CACHE_HEADERS + ), None + + # Regenerate the supported MFA list after + # registration/deregistration. + _mfa_auths = mfa_suppored_methods() + + return next_url, None, _mfa_auths + + +@pgCSRFProtect.exempt +@login_required +def registration_view() -> Response: + """ + A url end-point to register/deregister an authentication method. + + It supports two HTTP methods: + * GET : Generate a view listing all the suppoted list with 'Setup', + or 'Delete' buttons. If user has registered for the auth method, it + will render a 'Delete' button next to it, and 'Setup' button + otherwise. + * POST: This handles multiple scenarios on the registration page: + 1. Clicked on the 'Delete' button, it will deregister the user for + the specific auth method, and render the view same as for the + 'GET' method. + 2. Clicked on the 'Setup' button, it will render the registration + view for the authentication method. + 3. Clicked 'Continue' button, redirect it to the url specified by + 'next' url. + 4. Clicking on 'Cancel' button on the Auth method specific view + will render the view by 'GET' HTTP method. + 5. A registration method can run like a wizard, and generate + different views based on the request data. + + Returns: + Response: A response object with list of auth methods, a registration + view, or redirect to 'next' url + """ + mfa_auths = mfa_suppored_methods() + mfa_list = list() + + next_url = request.args.get("next", None) + + if request.method == 'POST': + next_url, response, mfa_auths = \ + __handle_registration_view_for_post_method(next_url, mfa_auths) + + if response is not None: + return response + + if next_url is None: + next_url = url_for(_INDEX_URL) + + error_message = None + found_one_mfa = False + + for key in mfa_auths: + mfa = mfa_auths[key]['mfa'] + mfa = mfa.to_dict() + mfa["registered"] = mfa_auths[key]["registered"] + mfa_list.append(mfa) + found_one_mfa = found_one_mfa or mfa["registered"] + + if request.method == 'GET': + if is_mfa_enabled() is False: + error_message = _( + "Can't access this page, when multi factor authentication is " + "disabled." + ) + elif is_mfa_session_authenticated() is False and \ + found_one_mfa is True: + flash(_("Complete the authentication process first"), "danger") + return redirect(login_url("mfa.validate", next_url=next_url)) + + return Response(render_template( + "mfa/register.html", _=_, + mfa_list=mfa_list, mfa_view=None, next_url=next_url, + error_message=error_message + ), 200 if error_message is None else 401, headers=_NO_CACHE_HEADERS) diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index fc24b546d..dda238d4d 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -25,6 +25,7 @@ from flask import current_app, render_template, url_for, make_response, \ from flask_babel import gettext from flask_gravatar import Gravatar from flask_login import current_user, login_required +from flask_login.utils import login_url from flask_security.changeable import change_user_password from flask_security.decorators import anonymous_user_required from flask_security.recoverable import reset_password_token_status, \ @@ -38,6 +39,8 @@ from werkzeug.datastructures import MultiDict import config from pgadmin import current_blueprint +from pgadmin.authenticate import get_logout_url +from pgadmin.authenticate.mfa.utils import mfa_required, is_mfa_enabled from pgadmin.settings import get_setting, store_setting from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response @@ -695,6 +698,7 @@ def check_browser_upgrade(): @blueprint.route("/") @pgCSRFProtect.exempt @login_required +@mfa_required def index(): """Render and process the main browser window.""" # Register Gravatar module with the app only if required @@ -753,7 +757,11 @@ def index(): username=current_user.username, auth_source=auth_source, is_admin=current_user.has_role("Administrator"), - logout_url=_get_logout_url(), + logout_url=get_logout_url(), + requirejs=True, + basejs=True, + mfa_enabled=is_mfa_enabled(), + login_url=login_url, _=gettext, auth_only_internal=auth_only_internal )) @@ -843,7 +851,7 @@ def utils(): app_version_int=config.APP_VERSION_INT, pg_libpq_version=pg_libpq_version, support_ssh_tunnel=config.SUPPORT_SSH_TUNNEL, - logout_url=_get_logout_url(), + logout_url=get_logout_url(), platform=sys.platform, qt_default_placeholder=QT_DEFAULT_PLACEHOLDER, enable_psql=config.ENABLE_PSQL diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 7553a0e23..316d3577c 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -22,7 +22,7 @@ define('pgadmin.browser', [ 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', 'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', - 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', + 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', 'pgadmin.browser.dialog', 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment', ], function( @@ -172,7 +172,7 @@ define('pgadmin.browser', [ let ih = window.innerHeight; if (ih > passed_height){ return passed_height; - }else{ + } else { if (ih > pgAdmin.Browser.stdH.lg) return pgAdmin.Browser.stdH.lg; else if (ih > pgAdmin.Browser.stdH.md) diff --git a/web/pgadmin/browser/static/js/dialog.js b/web/pgadmin/browser/static/js/dialog.js new file mode 100644 index 000000000..17ca7b17a --- /dev/null +++ b/web/pgadmin/browser/static/js/dialog.js @@ -0,0 +1,110 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import * as alertify from 'pgadmin.alertifyjs'; +import url_for from 'sources/url_for'; +import pgAdmin from 'sources/pgadmin'; + +let counter = 1; + +function url_dialog(_title, _url, _help_filename, _width, _height) { + + let pgBrowser = pgAdmin.Browser; + + const dlgName = 'UrlDialog' + counter++; + + alertify.dialog(dlgName, function factory() { + return { + main: function(_title) { + this.set({'title': _title}); + }, + build: function() { + alertify.pgDialogBuild.apply(this); + }, + settings: { + url: _url, + title: _title, + }, + setup: function() { + return { + buttons: [{ + text: '', + key: 112, + className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button', + attrs: { + name: 'dialog_help', + type: 'button', + label: _title, + url: url_for('help.static', { + 'filename': _help_filename, + }), + }, + }, { + text: gettext('Close'), + key: 27, + className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button', + attrs: { + name: 'close', + type: 'button', + }, + }], + // Set options for dialog + options: { + //disable both padding and overflow control. + padding: !1, + overflow: !1, + modal: false, + resizable: true, + maximizable: true, + pinnable: false, + closableByDimmer: false, + closable: false, + }, + }; + }, + hooks: { + // Triggered when the dialog is closed + onclose: function() { + // Clear the view + return setTimeout((function() { + return (alertify[dlgName]()).destroy(); + }), 1000); + }, + }, + prepare: function() { + // create the iframe element + var iframe = document.createElement('iframe'); + iframe.frameBorder = 'no'; + iframe.width = '100%'; + iframe.height = '100%'; + iframe.src = this.setting('url'); + // add it to the dialog + this.elements.content.appendChild(iframe); + }, + callback: function(e) { + if (e.button.element.name == 'dialog_help') { + e.cancel = true; + pgBrowser.showHelp( + e.button.element.name, e.button.element.getAttribute('url'), + null, null + ); + return; + } + }, + }; + }); + (alertify[dlgName](_title)).show().resizeTo(_width || pgBrowser.stdW.lg, _height || pgBrowser.stdH.md); +} + +pgAdmin.ui.dialogs.url_dialog = url_dialog; + +export { + url_dialog, +}; diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index c1387dad5..ee8fc6652 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -152,6 +152,17 @@ window.onload = function(e){ {% endif %} + {% if mfa_enabled is defined and mfa_enabled is true %} +
  • + {{ _('Two-Factor Authentication') }} +
  • + + {% endif %} {% if is_admin %}
  • {{ _('Users') }}
  • diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 58c309bbd..cfe1a6435 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -473,3 +473,11 @@ class UserMacros(db.Model): ) name = db.Column(db.String(1024), nullable=False) sql = db.Column(db.Text(), nullable=False) + + +class UserMFA(db.Model): + """Stores the options for the MFA for a particular user.""" + __tablename__ = 'user_mfa' + user_id = db.Column(db.Integer, db.ForeignKey(USER_ID), primary_key=True) + mfa_auth = db.Column(db.String(64), primary_key=True) + options = db.Column(db.Text(), nullable=True) diff --git a/web/pgadmin/static/img/login.svg b/web/pgadmin/static/img/login.svg index f502c59e0..64e5c4737 100644 --- a/web/pgadmin/static/img/login.svg +++ b/web/pgadmin/static/img/login.svg @@ -1 +1 @@ -login_graphic \ No newline at end of file +login_graphic diff --git a/web/pgadmin/static/js/pgadmin.js b/web/pgadmin/static/js/pgadmin.js index 4377b0a27..a9abcfa90 100644 --- a/web/pgadmin/static/js/pgadmin.js +++ b/web/pgadmin/static/js/pgadmin.js @@ -178,5 +178,7 @@ define([], function() { }; } + pgAdmin.ui = {dialogs: {}}; + return pgAdmin; }); diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss index c61e140aa..8b1e7bf8d 100644 --- a/web/pgadmin/static/scss/_pgadmin.style.scss +++ b/web/pgadmin/static/scss/_pgadmin.style.scss @@ -931,19 +931,31 @@ table.table-empty-rows{ } .login_page { - background-color: $color-primary; + background-color: $login-page-background; height: 100%; position:relative; z-index:1; color: $security-text-color; + & a { + color: $security-text-color; + } + + & .panel-container { + background-color: rgba($security-btn-color, 0.25); + padding: 0px; + } + & .panel-header { padding-bottom: 1.0rem; } & .panel-body { padding-bottom: 0.8rem; } - & .btn-login { + & .btn-login { + background-color: $security-btn-color; + } + & .btn-validate { background-color: $security-btn-color; } & .btn-oauth { @@ -977,8 +989,16 @@ table.table-empty-rows{ z-index: 100; } -.change_pass { +.auth_page { background-color: $color-gray-light; + + & .panel-container { + background-color: rgba($color-gray-dark, 0.75); + border-radius: $border-radius * 2; + } +} + +.change_pass { height: 100%; position:relative; z-index:1; diff --git a/web/pgadmin/static/scss/resources/_default.variables.scss b/web/pgadmin/static/scss/resources/_default.variables.scss index 0f885ccab..dbc7e12ab 100644 --- a/web/pgadmin/static/scss/resources/_default.variables.scss +++ b/web/pgadmin/static/scss/resources/_default.variables.scss @@ -366,6 +366,7 @@ $erd-canvas-grid: $color-gray !default; $erd-link-color: $color-fg !default; $erd-link-selected-color: $color-fg !default; +$login-page-background: $color-primary !default; @function url-friendly-colour($colour) { @return '%23' + str-slice('#{$colour}', 2, -1) diff --git a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss index 2fb335047..fa703decd 100644 --- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss +++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss @@ -83,6 +83,8 @@ $color-editor-activeline: #323e43 !default; $color-editor-activeline-light: $color-editor-activeline; $color-editor-activeline-border-color: none; +$login-page-background: $color-bg; + $explain-sev-2-bg: #ded17e; $explain-sev-3-bg: #824d18; $explain-sev-4-bg: #880000; diff --git a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss index 2f6c1f9d5..225945933 100644 --- a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss +++ b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss @@ -37,6 +37,8 @@ $color-gray: #1F2932; $color-gray-light: #2D3A48; $color-gray-lighter: #8B9CAD; +$login-page-background: $color-bg; + $sql-gutters-bg: $color-gray-light; $sql-title-bg: #1F2932; $sql-title-fg: $color-fg; diff --git a/web/pgadmin/templates/base.html b/web/pgadmin/templates/base.html index 65515d58a..8fce32a8e 100644 --- a/web/pgadmin/templates/base.html +++ b/web/pgadmin/templates/base.html @@ -32,6 +32,7 @@ window.resourceBasePath = "{{ url_for('static', filename='js') }}/generated/"; +{% if requirejs is defined and requirejs is true %} - +{% endif %} +{% if basejs is defined and basejs is true %} +{% endif %} @@ -74,7 +77,6 @@
    diff --git a/web/pgadmin/templates/security/change_password.html b/web/pgadmin/templates/security/change_password.html index 0420e9059..5917b799c 100644 --- a/web/pgadmin/templates/security/change_password.html +++ b/web/pgadmin/templates/security/change_password.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% from "security/fields.html" import render_field_with_errors %} {% block body %} -
    +
    {% include "security/messages.html" %}
    @@ -18,7 +18,7 @@ {{ render_field_with_errors(change_password_form.new_password, "password") }} {{ render_field_with_errors(change_password_form.new_password_confirm, "password") }} + title="{{ _('Change Password') }}" aria-label="{{ _('Change Password') }}> {% endif %} diff --git a/web/pgadmin/templates/security/panel.html b/web/pgadmin/templates/security/panel.html index 46aa0524c..d0fdef76e 100644 --- a/web/pgadmin/templates/security/panel.html +++ b/web/pgadmin/templates/security/panel.html @@ -2,7 +2,7 @@ {% from "security/fields.html" import render_field_with_errors, render_username_with_errors %} {% block title %}{{ config.APP_NAME }}{% endblock %} {% block body %} -