diff --git a/docs/en_US/getting_started.rst b/docs/en_US/getting_started.rst index 11ae656dc..700e8d48b 100644 --- a/docs/en_US/getting_started.rst +++ b/docs/en_US/getting_started.rst @@ -33,7 +33,6 @@ 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_auth_app.png b/docs/en_US/images/mfa_auth_app.png deleted file mode 100644 index e38fdc922..000000000 Binary files a/docs/en_US/images/mfa_auth_app.png and /dev/null differ diff --git a/docs/en_US/images/mfa_email.png b/docs/en_US/images/mfa_email.png deleted file mode 100644 index 81e3d20a2..000000000 Binary files a/docs/en_US/images/mfa_email.png and /dev/null differ diff --git a/docs/en_US/images/mfa_login.png b/docs/en_US/images/mfa_login.png deleted file mode 100644 index 0842dd799..000000000 Binary files a/docs/en_US/images/mfa_login.png and /dev/null differ diff --git a/docs/en_US/images/mfa_registration.png b/docs/en_US/images/mfa_registration.png deleted file mode 100644 index 91867a3fe..000000000 Binary files a/docs/en_US/images/mfa_registration.png and /dev/null differ diff --git a/docs/en_US/mfa.rst b/docs/en_US/mfa.rst deleted file mode 100644 index 5988f29d5..000000000 --- a/docs/en_US/mfa.rst +++ /dev/null @@ -1,88 +0,0 @@ -.. _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)." - - -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*. - -Email authentication -==================== - -To setup email authentication click on the `Setup` button besides of the -'Email authentication' label. - -.. image:: images/mfa_email.png - :alt: Configure two-factor authentication - :align: center - -Enter the valid email address to send the validation code. Once you get the -validation code enter that code to setup the email authentication. - -*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).* - -Authenticator App -================= - -To setup using any authenticator application which supports Time based One -Time Password (TOTP) click on the `Setup` button besides of the -'Authenticator App' label. - -.. image:: images/mfa_auth_app.png - :alt: Configure two-factor authentication - :align: center - -After the setup when you logged in to the pgAdmin 4 again, it will provide -the option to authenticate using email or authenticator app. - -.. image:: images/mfa_login.png - :alt: Configure two-factor authentication - :align: center diff --git a/docs/en_US/release_notes_6_0.rst b/docs/en_US/release_notes_6_0.rst index f609ddd4b..d384739fd 100644 --- a/docs/en_US/release_notes_6_0.rst +++ b/docs/en_US/release_notes_6_0.rst @@ -10,7 +10,6 @@ New features ************ | `Issue #4211 `_ - Added support for OWNED BY Clause for sequences. -| `Issue #6543 `_ - Added support for Two-factor authentication for improving security. Housekeeping ************ diff --git a/requirements.txt b/requirements.txt index 535736738..d45566a0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,6 +43,3 @@ 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 eab18762b..676a4f03f 100644 --- a/web/.eslintignore +++ b/web/.eslintignore @@ -4,4 +4,3 @@ vendor templates/ templates\ ycache -regression/htmlcov diff --git a/web/config.py b/web/config.py index 8200b7492..72ad01d14 100644 --- a/web/config.py +++ b/web/config.py @@ -729,29 +729,6 @@ OAUTH2_CONFIG = [ OAUTH2_AUTO_CREATE_USER = True -########################################################################## -# 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 340757c99..8cda71380 100644 --- a/web/migrations/script.py.mako +++ b/web/migrations/script.py.mako @@ -14,7 +14,8 @@ Revises: ${down_revision | comma,n} Create Date: ${create_date} """ -from pgadmin.model import db +from alembic import op +import sqlalchemy as sa ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/web/migrations/versions/15c88f765bc8_.py b/web/migrations/versions/15c88f765bc8_.py deleted file mode 100644 index 9775cd63d..000000000 --- a/web/migrations/versions/15c88f765bc8_.py +++ /dev/null @@ -1,44 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2021, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -"""Update DB to version 14 - -Added a table `user_mfa` for saving the options on MFA for different sources. - -Revision ID: 15c88f765bc8 -Revises: 6650c52670c2 -Create Date: 2021-06-22 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/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index 277424793..82c5ea9b0 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.model import db, User +from pgadmin import db, User from pgadmin.utils import PgAdminModule from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP from pgadmin.authenticate.registry import AuthSourceRegistry @@ -27,26 +27,6 @@ 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 deleted file mode 100644 index 5d9459d2c..000000000 --- a/web/pgadmin/authenticate/mfa/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -############################################################################## -# -# 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_babelex 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 deleted file mode 100644 index 6cc61e5b1..000000000 --- a/web/pgadmin/authenticate/mfa/authenticator.py +++ /dev/null @@ -1,222 +0,0 @@ -############################################################################## -# -# 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_babelex 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 then 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 deleted file mode 100644 index e75a2d8b5..000000000 --- a/web/pgadmin/authenticate/mfa/email.py +++ /dev/null @@ -1,310 +0,0 @@ -############################################################################## -# -# 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_babelex 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 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 deleted file mode 100644 index 1469551fb..000000000 --- a/web/pgadmin/authenticate/mfa/registry.py +++ /dev/null @@ -1,167 +0,0 @@ -########################################################################## -# -# 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 deleted file mode 100644 index 385b99b60..000000000 --- a/web/pgadmin/authenticate/mfa/static/images/email_lock.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/pgadmin/authenticate/mfa/static/images/totp_lock.svg b/web/pgadmin/authenticate/mfa/static/images/totp_lock.svg deleted file mode 100644 index fb3d685a9..000000000 --- a/web/pgadmin/authenticate/mfa/static/images/totp_lock.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email.js b/web/pgadmin/authenticate/mfa/templates/mfa/email.js deleted file mode 100644 index e6fe6f57b..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/email.js +++ /dev/null @@ -1,66 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// 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 deleted file mode 100644 index 0325ac9e5..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
{{ message }}
- -
- -
-
diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html b/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html deleted file mode 100644 index 3ecf635fb..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
{{ _("Verify with Email Authentication") }}
-
- -
-
diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/register.html b/web/pgadmin/authenticate/mfa/templates/mfa/register.html deleted file mode 100644 index 7b5173e97..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/register.html +++ /dev/null @@ -1,78 +0,0 @@ -{% 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 deleted file mode 100644 index acb07b004..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/validate.html +++ /dev/null @@ -1,121 +0,0 @@ -{% 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 deleted file mode 100644 index f9bf492d2..000000000 --- a/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.html +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index f9bf492d2..000000000 --- a/web/pgadmin/authenticate/mfa/templates/security/email/send_email_otp.txt +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 782dc923b..000000000 --- a/web/pgadmin/authenticate/mfa/tests/test_config.py +++ /dev/null @@ -1,154 +0,0 @@ -############################################################################## -# -# 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 deleted file mode 100644 index 01264f611..000000000 --- a/web/pgadmin/authenticate/mfa/tests/test_mfa.py +++ /dev/null @@ -1,56 +0,0 @@ -############################################################################## -# -# 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 deleted file mode 100644 index 12590e10b..000000000 --- a/web/pgadmin/authenticate/mfa/tests/test_mfa_view.py +++ /dev/null @@ -1,66 +0,0 @@ -############################################################################## -# -# 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 deleted file mode 100644 index fbf3e8c61..000000000 --- a/web/pgadmin/authenticate/mfa/tests/test_user_execution.py +++ /dev/null @@ -1,125 +0,0 @@ -############################################################################## -# -# 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 deleted file mode 100644 index 1bd15ebf0..000000000 --- a/web/pgadmin/authenticate/mfa/tests/utils.py +++ /dev/null @@ -1,111 +0,0 @@ -############################################################################## -# -# 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 deleted file mode 100644 index f3e8f9b53..000000000 --- a/web/pgadmin/authenticate/mfa/utils.py +++ /dev/null @@ -1,408 +0,0 @@ -############################################################################## -# -# 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 deleted file mode 100644 index 6ae7272d9..000000000 --- a/web/pgadmin/authenticate/mfa/views.py +++ /dev/null @@ -1,346 +0,0 @@ -############################################################################## -# -# 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_babelex 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 6e77454a2..084fb178a 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -25,7 +25,6 @@ from flask import current_app, render_template, url_for, make_response, \ from flask_babelex 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, \ @@ -39,8 +38,6 @@ 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 @@ -698,7 +695,6 @@ 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 @@ -758,11 +754,7 @@ def index(): username=current_user.username, auth_source=auth_source, is_admin=current_user.has_role("Administrator"), - logout_url=get_logout_url(), - requirejs=True, - basejs=True, - mfa_enabled=is_mfa_enabled(), - login_url=login_url, + logout_url=_get_logout_url(), _=gettext, auth_only_internal=auth_only_internal )) @@ -856,7 +848,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 a0bef78bd..90398e3a8 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -19,7 +19,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', 'pgadmin.browser.dialog', + 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment', ], function( @@ -160,7 +160,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 deleted file mode 100644 index 17ca7b17a..000000000 --- a/web/pgadmin/browser/static/js/dialog.js +++ /dev/null @@ -1,110 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// 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 98f7803d1..c1387dad5 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -152,17 +152,6 @@ 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 f79cc31ef..aea32c1e1 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -473,11 +473,3 @@ 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 64e5c4737..f502c59e0 100644 --- a/web/pgadmin/static/img/login.svg +++ b/web/pgadmin/static/img/login.svg @@ -1 +1 @@ -login_graphic +login_graphic \ No newline at end of file diff --git a/web/pgadmin/static/js/pgadmin.js b/web/pgadmin/static/js/pgadmin.js index a9abcfa90..4377b0a27 100644 --- a/web/pgadmin/static/js/pgadmin.js +++ b/web/pgadmin/static/js/pgadmin.js @@ -178,7 +178,5 @@ 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 88a9e2f64..9266f82e4 100644 --- a/web/pgadmin/static/scss/_pgadmin.style.scss +++ b/web/pgadmin/static/scss/_pgadmin.style.scss @@ -932,31 +932,19 @@ table.table-empty-rows{ } .login_page { - background-color: $login-page-background; + background-color: $color-primary; 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 { - background-color: $security-btn-color; - } - & .btn-validate { + & .btn-login { background-color: $security-btn-color; } & .btn-oauth { @@ -990,16 +978,8 @@ table.table-empty-rows{ z-index: 100; } -.auth_page { - background-color: $color-gray-light; - - & .panel-container { - background-color: rgba($color-gray-dark, 0.75); - border-radius: $border-radius * 2; - } -} - .change_pass { + background-color: $color-gray-light; 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 8a9517616..7b187a45c 100644 --- a/web/pgadmin/static/scss/resources/_default.variables.scss +++ b/web/pgadmin/static/scss/resources/_default.variables.scss @@ -364,7 +364,6 @@ $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 f4e0bc70d..2c8534553 100644 --- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss +++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss @@ -81,8 +81,6 @@ $color-editor-operator: #d6aaaa; $color-editor-foldmarker: #0000FF !default; $color-editor-activeline: #323e43 !default; -$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 86001793c..cd6462df4 100644 --- a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss +++ b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss @@ -37,8 +37,6 @@ $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 fad6ec5f1..991b745b3 100644 --- a/web/pgadmin/templates/base.html +++ b/web/pgadmin/templates/base.html @@ -32,7 +32,6 @@ 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 %} @@ -76,6 +73,7 @@ {% block body %}{% endblock %} diff --git a/web/pgadmin/templates/security/change_password.html b/web/pgadmin/templates/security/change_password.html index 5917b799c..0420e9059 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") }} {% endif %} diff --git a/web/pgadmin/templates/security/panel.html b/web/pgadmin/templates/security/panel.html index d0fdef76e..46aa0524c 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 %} -