From 48ca83f31dee76c05edb6b64a595964968ec7eb2 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Tue, 6 Jul 2021 13:22:58 +0530 Subject: [PATCH] Added support for OAuth 2 authentication. Fixes #5940 Initial patch sent by: Florian Sabonchi --- DEPENDENCIES | 1 + docs/en_US/getting_started.rst | 1 + docs/en_US/images/oauth2_login.png | Bin 0 -> 112129 bytes docs/en_US/kerberos.rst | 11 ++ docs/en_US/oauth2.rst | 61 ++++++ docs/en_US/release_notes_5_5.rst | 1 + requirements.txt | 2 + web/config.py | 48 ++++- web/pgadmin/__init__.py | 21 +- web/pgadmin/authenticate/__init__.py | 181 +++++------------- web/pgadmin/authenticate/internal.py | 6 +- web/pgadmin/authenticate/kerberos.py | 126 +++++++++++- web/pgadmin/authenticate/oauth2.py | 172 +++++++++++++++++ .../authenticate/static/js/kerberos.js | 13 +- web/pgadmin/browser/__init__.py | 27 ++- .../tests/test_role_dependencies_sql.py | 10 +- .../browser/templates/browser/js/constants.js | 3 +- .../tests/test_kerberos_with_mocking.py | 22 ++- web/pgadmin/browser/tests/test_ldap_login.py | 6 +- .../browser/tests/test_ldap_with_mocking.py | 16 +- web/pgadmin/browser/tests/test_login.py | 3 +- .../browser/tests/test_master_password.py | 2 + .../browser/tests/test_oauth2_with_mocking.py | 147 ++++++++++++++ web/pgadmin/misc/bgprocess/processes.py | 2 +- web/pgadmin/static/scss/_pgadmin.style.scss | 3 + .../templates/security/login_user.html | 11 +- web/pgadmin/tools/user_management/__init__.py | 2 +- .../static/js/user_management.js | 13 +- web/pgadmin/utils/constants.py | 4 +- .../utils/driver/psycopg2/connection.py | 6 +- web/pgadmin/utils/master_password.py | 10 +- .../python_test_utils/csrf_test_client.py | 27 ++- .../python_test_utils/test_utils.py | 6 +- web/regression/runtests.py | 3 +- web/yarn.lock | 4 +- 35 files changed, 747 insertions(+), 224 deletions(-) create mode 100644 docs/en_US/images/oauth2_login.png create mode 100644 docs/en_US/oauth2.rst create mode 100644 web/pgadmin/authenticate/oauth2.py create mode 100644 web/pgadmin/browser/tests/test_oauth2_with_mocking.py diff --git a/DEPENDENCIES b/DEPENDENCIES index 3aaea84c4ad..9eace7eed14 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -56,6 +56,7 @@ eventlet 0.31.0 httpagentparser 1.9.1 http://www.opensource.org/licenses/mit-license.php http://shon.github.com/httpagentparser user-agents 2.2.0 MIT https://github.com/selwin/python-user-agents pywinpty 1.1.1 Unknown Unknown +authlib 0.15.3 BSD https://github.com/lepture/authlib NOTE: This report was generated using Python 3.9. Full information may not be shown for Python modules that are not required with this version. diff --git a/docs/en_US/getting_started.rst b/docs/en_US/getting_started.rst index b135fe082d9..e3d0edc969d 100644 --- a/docs/en_US/getting_started.rst +++ b/docs/en_US/getting_started.rst @@ -37,6 +37,7 @@ Mode is pre-configured for security. change_user_password ldap kerberos + oauth2 .. note:: Pre-compiled and configured installation packages are available for diff --git a/docs/en_US/images/oauth2_login.png b/docs/en_US/images/oauth2_login.png new file mode 100644 index 0000000000000000000000000000000000000000..50ecff576455b5f06af10abbd046fe0e49d8e67f GIT binary patch literal 112129 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 Ii5dm}e?M4v4FCWD literal 0 HcmV?d00001 diff --git a/docs/en_US/kerberos.rst b/docs/en_US/kerberos.rst index 1116c467231..6dcbd674cb6 100644 --- a/docs/en_US/kerberos.rst +++ b/docs/en_US/kerberos.rst @@ -34,6 +34,7 @@ from *config.py* file and modify the values for the following parameters. Please note that if it is not set, it will take the value of *default_server* parameter." + Keytab file for HTTP Service ============================ @@ -116,3 +117,13 @@ PostgreSQL Server settings to configure Kerberos Authentication * Note that, you have to login into pgAdmin with Kerberos authentication to then connect to PostgreSQL using Kerberos. + + +Master Password +=============== + +In the multi user mode, pgAdmin uses user's login password to encrypt/decrypt the PostgreSQL server password. +In the Kerberos authentication, the pgAdmin user does not have the password, so we need an encryption key to store +the PostgreSQL server password for the servers which are not configured to use the Kerberos authentication. +To accomplish this, set the configuration parameter MASTER_PASSWORD to *True*, so upon setting the master password, +it will be used as an encryption key while storing the password. If it is False, the server password can not be stored. diff --git a/docs/en_US/oauth2.rst b/docs/en_US/oauth2.rst new file mode 100644 index 00000000000..8947b509ee9 --- /dev/null +++ b/docs/en_US/oauth2.rst @@ -0,0 +1,61 @@ +.. _oauth2: + +***************************************** +`Enabling OAUTH2 Authentication`:index: +***************************************** + + +To enable OAUTH2 authentication for pgAdmin, you must configure the OAUTH2 +settings in the *config_local.py* or *config_system.py* file (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 + + "AUTHENTICATION_SOURCES", "The default value for this parameter is *internal*. + To enable OAUTH2 authentication, you must include *oauth2* in the list of values + for this parameter. you can modify the value as follows: + + * [‘oauth2’, ‘internal’]: pgAdmin will display an additional button for authenticating with oauth2" + "OAUTH2_NAME", "The name of the Oauth2 provider, ex: Google, Github" + "OAUTH2_DISPLAY_NAME", "Oauth2 display name in pgAdmin" + "OAUTH2_CLIENT_ID", "Oauth2 Client ID" + "OAUTH2_CLIENT_SECRET", "Oauth2 Client Secret" + "OAUTH2_TOKEN_URL", "Oauth2 Access Token endpoint" + "OAUTH2_AUTHORIZATION_URL", "Endpoint for user authorization" + "OAUTH2_API_BASE_URL", "Oauth2 base URL endpoint to make requests simple, ex: *https://api.github.com/*" + "OAUTH2_USERINFO_ENDPOINT", "User Endpoint, ex: *user* (for github) and *useinfo* (for google)" + "OAUTH2_ICON", "The Font-awesome icon to be placed on the oauth2 button, ex: fa-github" + "OAUTH2_BUTTON_COLOR", "Oauth2 button color" + "OAUTH2_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically + create a pgAdmin user corresponding to a successfully authenticated Oauth2 user. + Please note that password is not stored in the pgAdmin database." + +Redirect URL +============ + +The redirect url to configure Oauth2 server is *http:///oauth2/authorize* + +Master Password +=============== + +In the multi user mode, pgAdmin uses user's login password to encrypt/decrypt the PostgreSQL server password. +In the Oauth2 authentication, the pgAdmin does not store the user's password, so we need an encryption key to store +the PostgreSQL server password. +To accomplish this, set the configuration parameter MASTER_PASSWORD to *True*, so upon setting the master password, +it will be used as an encryption key while storing the password. If it is False, the server password can not be stored. + + +Login Page +============ + +After configuration, on restart, you can see the login page with the Oauth2 login button(s). + +.. image:: images/oauth2_login.png + :alt: Oauth2 login + :align: center diff --git a/docs/en_US/release_notes_5_5.rst b/docs/en_US/release_notes_5_5.rst index 5ce8170344f..25b01923a7c 100644 --- a/docs/en_US/release_notes_5_5.rst +++ b/docs/en_US/release_notes_5_5.rst @@ -12,6 +12,7 @@ New features | `Issue #1975 `_ - Highlighted long running queries on the dashboards. | `Issue #3893 `_ - Added support for Reassign/Drop Owned for login roles. | `Issue #3920 `_ - Do not block the query editor window when running a query. +| `Issue #5940 `_ - Added support for OAuth 2 authentication. | `Issue #6559 `_ - Added option to provide maximum width of the column when 'Resize by data?’ option in the preferences is set to True. Housekeeping diff --git a/requirements.txt b/requirements.txt index e6022d8f149..d45566a0bc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,3 +41,5 @@ eventlet==0.31.0 httpagentparser==1.9.* user-agents==2.2.0 pywinpty==1.1.1; sys_platform=="win32" +Authlib==0.15.* +requests==2.25.* diff --git a/web/config.py b/web/config.py index 1c65a318081..3b730733b18 100644 --- a/web/config.py +++ b/web/config.py @@ -562,10 +562,11 @@ ########################################################################## # Default setting is internal -# External Supported Sources: ldap, kerberos +# External Supported Sources: ldap, kerberos, oauth2 # Multiple authentication can be achieved by setting this parameter to -# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first, -# in case of failure internal authentication will be done. +# ['ldap', 'internal'] or ['oauth2', 'internal'] etc. +# pgAdmin will authenticate the user with ldap/oauth2 whatever first in the +# list, in case of failure the second authentication option will be considered. AUTHENTICATION_SOURCES = ['internal'] @@ -666,6 +667,47 @@ KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache') +########################################################################## +# OAuth2 Configuration +########################################################################## + +# Multiple OAUTH2 providers can be added in the list like [{...},{...}] +# All parameters are required + +OAUTH2_CONFIG = [ + { + # The name of the of the oauth provider, ex: github, google + 'OAUTH2_NAME': None, + # The display name, ex: Google + 'OAUTH2_DISPLAY_NAME': '', + # Oauth client id + 'OAUTH2_CLIENT_ID': None, + # Oauth secret + 'OAUTH2_CLIENT_SECRET': None, + # URL to generate a token, + # Ex: https://github.com/login/oauth/access_token + 'OAUTH2_TOKEN_URL': None, + # URL is used for authentication, + # Ex: https://github.com/login/oauth/authorize + 'OAUTH2_AUTHORIZATION_URL': None, + # Oauth base url, ex: https://api.github.com/ + 'OAUTH2_API_BASE_URL': None, + # Name of the Endpoint, ex: user + 'OAUTH2_USERINFO_ENDPOINT': None, + # Font-awesome icon, ex: fa-github + 'OAUTH2_ICON': None, + # UI button colour, ex: #0000ff + 'OAUTH2_BUTTON_COLOR': None, + } +] + +# After Oauth authentication, user will be added into the SQLite database +# automatically, if set to True. +# Set it to False, if user should not be added automatically, +# in this case Admin has to add the user manually in the SQLite database. + +OAUTH2_AUTO_CREATE_USER = True + ########################################################################## # PSQL tool settings ########################################################################## diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 8bfb18d399d..2a1c959fd43 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -46,12 +46,13 @@ from pgadmin.utils.csrf import pgCSRFProtect from pgadmin import authenticate from pgadmin.utils.security_headers import SecurityHeaders -from pgadmin.utils.constants import KERBEROS +from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP # Explicitly set the mime-types so that a corrupted windows registry will not # affect pgAdmin 4 to be load properly. This will avoid the issues that may # occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff". import mimetypes + mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') @@ -469,6 +470,13 @@ def upgrade_db(): 'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS })) + app.config.update(dict({ + 'INTERNAL': INTERNAL, + 'LDAP': LDAP, + 'KERBEROS': KERBEROS, + 'OAUTH2': OAUTH2 + })) + security.init_app(app, user_datastore) # register custom unauthorised handler. @@ -760,19 +768,18 @@ def before_request(): ) abort(401) login_user(user) - elif config.SERVER_MODE and\ - app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\ - KERBEROS and \ + elif config.SERVER_MODE and \ not current_user.is_authenticated and \ request.endpoint in ('redirects.index', 'security.login'): - return authenticate.login() - + if app.PGADMIN_EXTERNAL_AUTH_SOURCE == KERBEROS: + return authenticate.login() # if the server is restarted the in memory key will be lost # but the user session may still be active. Logout the user # to get the key again when login if config.SERVER_MODE and current_user.is_authenticated and \ app.PGADMIN_EXTERNAL_AUTH_SOURCE != \ - KERBEROS and \ + KERBEROS and app.PGADMIN_EXTERNAL_AUTH_SOURCE != \ + OAUTH2 and\ current_app.keyManager.get() is None and \ request.endpoint not in ('security.login', 'security.logout'): logout_user() diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index 101d5765609..b512eb2ce15 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -9,68 +9,33 @@ """A blueprint module implementing the Authentication.""" -import flask -import pickle +import config +import copy + from flask import current_app, flash, Response, request, url_for,\ - render_template + session, redirect from flask_babelex import gettext -from flask_security import current_user, login_required -from flask_security.views import _security, _ctx -from flask_security.utils import config_value, get_post_logout_redirect, \ - get_post_login_redirect, logout_user -from pgadmin.utils.ajax import make_json_response, internal_server_error -import os +from flask_security.views import _security +from flask_security.utils import get_post_logout_redirect, \ + get_post_login_redirect -from flask import session - -import config from pgadmin.utils import PgAdminModule -from pgadmin.utils.constants import KERBEROS, INTERNAL -from pgadmin.utils.csrf import pgCSRFProtect +from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP +from pgadmin.authenticate.registry import AuthSourceRegistry -from .registry import AuthSourceRegistry MODULE_NAME = 'authenticate' +auth_obj = None class AuthenticateModule(PgAdminModule): def get_exposed_url_endpoints(self): - return ['authenticate.login', - 'authenticate.kerberos_login', - 'authenticate.kerberos_logout', - 'authenticate.kerberos_update_ticket', - 'authenticate.kerberos_validate_ticket'] + return ['authenticate.login'] blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='') -@blueprint.route("/login/kerberos", - endpoint="kerberos_login", methods=["GET"]) -@pgCSRFProtect.exempt -def kerberos_login(): - logout_user() - return Response(render_template("browser/kerberos_login.html", - login_url=url_for('security.login'), - )) - - -@blueprint.route("/logout/kerberos", - endpoint="kerberos_logout", methods=["GET"]) -@pgCSRFProtect.exempt -def kerberos_logout(): - logout_user() - if 'KRB5CCNAME' in session: - # Remove the credential cache - cache_file_path = session['KRB5CCNAME'].split(":")[1] - if os.path.exists(cache_file_path): - os.remove(cache_file_path) - - return Response(render_template("browser/kerberos_logout.html", - login_url=url_for('security.login'), - )) - - @blueprint.route('/login', endpoint='login', methods=['GET', 'POST']) def login(): """ @@ -78,15 +43,20 @@ def login(): The user input will be validated and authenticated. """ form = _security.login_form() - auth_obj = AuthSourceManager(form, config.AUTHENTICATION_SOURCES) - session['_auth_source_manager_obj'] = None + auth_obj = AuthSourceManager(form, copy.deepcopy( + config.AUTHENTICATION_SOURCES)) + if OAUTH2 in config.AUTHENTICATION_SOURCES\ + and 'oauth2_button' in request.form: + session['auth_obj'] = auth_obj + + session['auth_source_manager'] = None # Validate the user if not auth_obj.validate(): for field in form.errors: for error in form.errors[field]: flash(error, 'warning') - return flask.redirect(get_post_logout_redirect()) + return redirect(get_post_logout_redirect()) # Authenticate the user status, msg = auth_obj.authenticate() @@ -94,34 +64,40 @@ def login(): # Login the user status, msg = auth_obj.login() current_auth_obj = auth_obj.as_dict() + if not status: if current_auth_obj['current_source'] ==\ KERBEROS: - return flask.redirect('{0}?next={1}'.format(url_for( + return redirect('{0}?next={1}'.format(url_for( 'authenticate.kerberos_login'), url_for('browser.index'))) flash(msg, 'danger') - return flask.redirect(get_post_logout_redirect()) - - session['_auth_source_manager_obj'] = current_auth_obj - return flask.redirect(get_post_login_redirect()) + return redirect(get_post_logout_redirect()) + session['auth_source_manager'] = current_auth_obj + if 'auth_obj' in session: + session['auth_obj'] = None + return redirect(get_post_login_redirect()) elif isinstance(msg, Response): return msg + elif 'oauth2_button' in request.form and not isinstance(msg, str): + return msg flash(msg, 'danger') - response = flask.redirect(get_post_logout_redirect()) + response = redirect(get_post_logout_redirect()) return response -class AuthSourceManager(): +class AuthSourceManager: """This class will manage all the authentication sources. """ + def __init__(self, form, sources): self.form = form self.auth_sources = sources self.source = None self.source_friendly_name = INTERNAL - self.current_source = None + self.current_source = INTERNAL + self.update_auth_sources() def as_dict(self): """ @@ -135,6 +111,14 @@ def as_dict(self): return res + def update_auth_sources(self): + for auth_src in [KERBEROS, OAUTH2]: + if auth_src in self.auth_sources: + if 'internal_button' in request.form: + self.auth_sources.remove(auth_src) + elif INTERNAL in self.auth_sources: + self.auth_sources.remove(INTERNAL) + def set_current_source(self, source): self.current_source = source @@ -170,36 +154,19 @@ def authenticate(self): msg = None for src in self.auth_sources: source = get_auth_sources(src) + self.set_source(source) current_app.logger.debug( "Authentication initiated via source: %s" % source.get_source_name()) - if self.form.data['email'] and self.form.data['password'] and \ - source.get_source_name() == KERBEROS: - msg = gettext('pgAdmin internal user authentication' - ' is not enabled, please contact administrator.') - continue - status, msg = source.authenticate(self.form) - # When server sends Unauthorized header to get the ticket over HTTP - # OR When kerberos authentication failed while accessing pgadmin, - # we need to break the loop as no need to authenticate further - # even if the authentication sources set to multiple - if not status: - if (hasattr(msg, 'status') and - msg.status == '401 UNAUTHORIZED') or\ - (source.get_source_name() == - KERBEROS and - request.method == 'GET'): - break - if status: - self.set_source(source) self.set_current_source(source.get_source_name()) if msg is not None and 'username' in msg: self.form._fields['email'].data = msg['username'] return status, msg + return status, msg def login(self): @@ -209,6 +176,13 @@ def login(self): current_app.logger.debug( "Authentication and Login successfully done via source : %s" % self.source.get_source_name()) + + # Set the login, logout view as per source if available + current_app.login_manager.login_view = getattr( + self.source, 'LOGIN_VIEW', 'security.login') + current_app.login_manager.logout_view = getattr( + self.source, 'LOGOUT_VIEW', 'security.logout') + return status, msg @@ -239,58 +213,3 @@ def init_app(app): AuthSourceRegistry.load_modules(app) return auth_sources - - -@blueprint.route("/kerberos/update_ticket", - endpoint="kerberos_update_ticket", methods=["GET"]) -@pgCSRFProtect.exempt -@login_required -def kerberos_update_ticket(): - """ - Update the kerberos ticket. - """ - from werkzeug.datastructures import Headers - headers = Headers() - - authorization = request.headers.get("Authorization", None) - - if authorization is None: - # Send the Negotiate header to the client - # if Kerberos ticket is not found. - headers.add('WWW-Authenticate', 'Negotiate') - return Response("Unauthorised", 401, headers) - else: - source = get_auth_sources(KERBEROS) - auth_header = authorization.split() - in_token = auth_header[1] - - # Validate the Kerberos ticket - status, context = source.negotiate_start(in_token) - if status: - return Response("Ticket updated successfully.") - - return Response(context, 500) - - -@blueprint.route("/kerberos/validate_ticket", - endpoint="kerberos_validate_ticket", methods=["GET"]) -@pgCSRFProtect.exempt -@login_required -def kerberos_validate_ticket(): - """ - Return the kerberos ticket lifetime left after getting the - ticket from the credential cache - """ - import gssapi - - try: - del_creds = gssapi.Credentials(store={'ccache': session['KRB5CCNAME']}) - creds = del_creds.acquire(store={'ccache': session['KRB5CCNAME']}) - except Exception as e: - current_app.logger.exception(e) - return internal_server_error(errormsg=str(e)) - - return make_json_response( - data={'ticket_lifetime': creds.lifetime}, - status=200 - ) diff --git a/web/pgadmin/authenticate/internal.py b/web/pgadmin/authenticate/internal.py index 484a7fdca95..515f00a5dee 100644 --- a/web/pgadmin/authenticate/internal.py +++ b/web/pgadmin/authenticate/internal.py @@ -10,7 +10,7 @@ """Implements Internal Authentication""" import six -from flask import current_app +from flask import current_app, flash from flask_security import login_user from abc import abstractmethod, abstractproperty from flask_babelex import gettext @@ -31,6 +31,8 @@ class BaseAuthentication(object): 'PASSWORD_NOT_PROVIDED': gettext('Password not provided'), 'INVALID_EMAIL': gettext('Email/Username is not valid') } + LOGIN_VIEW = 'security.login' + LOGOUT_VIEW = 'security.logout' @abstractmethod def get_source_name(self): @@ -97,7 +99,7 @@ def validate(self, form): """User validation""" # validate the email id first if not validate_email(form.data['email']): - form.errors['email'] = [self.messages('INVALID_EMAIL')] + flash(self.messages('INVALID_EMAIL'), 'warning') return False # Flask security validation return form.validate_on_submit() diff --git a/web/pgadmin/authenticate/kerberos.py b/web/pgadmin/authenticate/kerberos.py index 2f8fd0d6e37..a018f473e02 100644 --- a/web/pgadmin/authenticate/kerberos.py +++ b/web/pgadmin/authenticate/kerberos.py @@ -10,22 +10,28 @@ """A blueprint module implementing the Spnego/Kerberos authentication.""" import base64 -from os import environ, path +from os import environ, path, remove -from werkzeug.datastructures import Headers +from werkzeug.datastructures import Headers, MultiDict from flask_babelex import gettext -from flask import Flask, request, Response, session,\ - current_app, render_template, flash +from flask import request, Response, session,\ + current_app, render_template, flash, url_for +from flask_security.views import _security +from flask_security.utils import logout_user +from flask_security import login_required import config from pgadmin.model import User from pgadmin.tools.user_management import create_user from pgadmin.utils.constants import KERBEROS +from pgadmin.utils import PgAdminModule +from pgadmin.utils.ajax import make_json_response, internal_server_error -from flask_security.views import _security -from werkzeug.datastructures import MultiDict -from .internal import BaseAuthentication +from pgadmin.authenticate.internal import BaseAuthentication +from pgadmin.authenticate import get_auth_sources +from pgadmin.utils.csrf import pgCSRFProtect + try: import gssapi @@ -46,8 +52,110 @@ environ['KRB5_KTNAME'] = config.KRB_KTNAME +class KerberosModule(PgAdminModule): + def register(self, app, options, first_registration=False): + # Do not look for the sub_modules, + # instead call blueprint.register(...) directly + super(PgAdminModule, self).register(app, options, first_registration) + + def get_exposed_url_endpoints(self): + return ['kerberos.login', + 'kerberos.logout', + 'kerberos.update_ticket', + 'kerberos.validate_ticket'] + + +def init_app(app): + MODULE_NAME = 'kerberos' + + blueprint = KerberosModule(MODULE_NAME, __name__, static_url_path='') + + @blueprint.route("/login", + endpoint="login", methods=["GET"]) + @pgCSRFProtect.exempt + def kerberos_login(): + logout_user() + return Response(render_template("browser/kerberos_login.html", + login_url=url_for('security.login'), + )) + + @blueprint.route("/logout", + endpoint="logout", methods=["GET"]) + @pgCSRFProtect.exempt + def kerberos_logout(): + logout_user() + if 'KRB5CCNAME' in session: + # Remove the credential cache + cache_file_path = session['KRB5CCNAME'].split(":")[1] + if path.exists(cache_file_path): + remove(cache_file_path) + + return Response(render_template("browser/kerberos_logout.html", + login_url=url_for('security.login'), + )) + + @blueprint.route("/update_ticket", + endpoint="update_ticket", methods=["GET"]) + @pgCSRFProtect.exempt + @login_required + def kerberos_update_ticket(): + """ + Update the kerberos ticket. + """ + from werkzeug.datastructures import Headers + headers = Headers() + + authorization = request.headers.get("Authorization", None) + + if authorization is None: + # Send the Negotiate header to the client + # if Kerberos ticket is not found. + headers.add('WWW-Authenticate', 'Negotiate') + return Response("Unauthorised", 401, headers) + else: + source = get_auth_sources(KERBEROS) + auth_header = authorization.split() + in_token = auth_header[1] + + # Validate the Kerberos ticket + status, context = source.negotiate_start(in_token) + if status: + return Response("Ticket updated successfully.") + + return Response(context, 500) + + @blueprint.route("/validate_ticket", + endpoint="validate_ticket", methods=["GET"]) + @pgCSRFProtect.exempt + @login_required + def kerberos_validate_ticket(): + """ + Return the kerberos ticket lifetime left after getting the + ticket from the credential cache + """ + import gssapi + + try: + del_creds = gssapi.Credentials(store={ + 'ccache': session['KRB5CCNAME']}) + creds = del_creds.acquire(store={'ccache': session['KRB5CCNAME']}) + except Exception as e: + current_app.logger.exception(e) + return internal_server_error(errormsg=str(e)) + + return make_json_response( + data={'ticket_lifetime': creds.lifetime}, + status=200 + ) + + app.register_blueprint(blueprint) + + class KerberosAuthentication(BaseAuthentication): + LOGIN_VIEW = 'kerberos.login' + LOGOUT_VIEW = 'kerberos.logout' + def get_source_name(self): return KERBEROS @@ -85,7 +193,7 @@ def authenticate(self, frm): if status: # Saving the first 15 characters of the kerberos key # to encrypt/decrypt database password - session['kerberos_key'] = auth_header[1][0:15] + session['pass_enc_key'] = auth_header[1][0:15] # Create user retval = self.__auto_create_user( str(negotiate.initiator_name)) @@ -162,7 +270,7 @@ def __auto_create_user(self, username): username = str(username) if config.KRB_AUTO_CREATE_USER: user = User.query.filter_by( - username=username).first() + username=username, auth_source=KERBEROS).first() if user is None: return create_user({ 'username': username, diff --git a/web/pgadmin/authenticate/oauth2.py b/web/pgadmin/authenticate/oauth2.py new file mode 100644 index 00000000000..67c00ba71d1 --- /dev/null +++ b/web/pgadmin/authenticate/oauth2.py @@ -0,0 +1,172 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module implementing the Oauth2 authentication.""" + +import config + +from authlib.integrations.flask_client import OAuth +from flask import current_app, url_for, session, request,\ + redirect, Flask, flash +from flask_babelex import gettext +from flask_security import login_user, current_user +from flask_security.utils import get_post_logout_redirect, \ + get_post_login_redirect, logout_user + +from pgadmin.authenticate.internal import BaseAuthentication +from pgadmin.model import User +from pgadmin.tools.user_management import create_user +from pgadmin.utils.constants import OAUTH2 +from pgadmin.utils import PgAdminModule +from pgadmin.utils.csrf import pgCSRFProtect +from pgadmin.model import db + +OAUTH2_LOGOUT = 'oauth2.logout' +OAUTH2_AUTHORIZE = 'oauth2.authorize' + + +class Oauth2Module(PgAdminModule): + def register(self, app, options, first_registration=False): + # Do not look for the sub_modules, + # instead call blueprint.register(...) directly + super(PgAdminModule, self).register(app, options, first_registration) + + def get_exposed_url_endpoints(self): + return [OAUTH2_AUTHORIZE, + OAUTH2_LOGOUT] + + +def init_app(app): + MODULE_NAME = 'oauth2' + + blueprint = Oauth2Module(MODULE_NAME, __name__, static_url_path='') + + @blueprint.route('/authorize', endpoint="authorize", + methods=['GET', 'POST']) + @pgCSRFProtect.exempt + def oauth_authorize(): + auth_obj = session['auth_obj'] + auth_obj.set_current_source(auth_obj.source.get_source_name()) + status, msg = auth_obj.login() + if status: + session['auth_source_manager'] = auth_obj.as_dict() + session['auth_obj'] = None + return redirect(get_post_login_redirect()) + logout_user() + flash(msg) + return redirect(get_post_login_redirect()) + + @blueprint.route('/logout', endpoint="logout", + methods=['GET', 'POST']) + @pgCSRFProtect.exempt + def oauth_logout(): + if not current_user.is_authenticated: + return redirect(get_post_logout_redirect()) + for key in list(session.keys()): + session.pop(key) + logout_user() + return redirect(get_post_logout_redirect()) + + app.register_blueprint(blueprint) + app.login_manager.logout_view = OAUTH2_LOGOUT + + +class OAuth2Authentication(BaseAuthentication): + """OAuth Authentication Class""" + + LOGOUT_VIEW = OAUTH2_LOGOUT + + oauth_obj = OAuth(Flask(__name__)) + oauth2_clients = {} + oauth2_config = {} + + def __init__(self): + for oauth2_config in config.OAUTH2_CONFIG: + + OAuth2Authentication.oauth2_config[ + oauth2_config['OAUTH2_NAME']] = oauth2_config + + OAuth2Authentication.oauth2_clients[ + oauth2_config['OAUTH2_NAME'] + ] = OAuth2Authentication.oauth_obj.register( + name=oauth2_config['OAUTH2_NAME'], + client_id=oauth2_config['OAUTH2_CLIENT_ID'], + client_secret=oauth2_config['OAUTH2_CLIENT_SECRET'], + access_token_url=oauth2_config['OAUTH2_TOKEN_URL'], + authorize_url=oauth2_config['OAUTH2_AUTHORIZATION_URL'], + api_base_url=oauth2_config['OAUTH2_API_BASE_URL'], + client_kwargs={'scope': 'email profile'} + ) + + def get_source_name(self): + return OAUTH2 + + def get_friendly_name(self): + return self.oauth2_config[self.oauth2_current_client]['OAUTH2_NAME'] + + def validate(self, form): + return True + + def login(self, form): + profile = self.get_user_profile() + print(profile) + if 'email' not in profile or not profile['email']: + current_app.logger.exception( + 'An email is required for authentication' + ) + return False, gettext( + "An email is required for the oauth authentication.") + + user, msg = self.__auto_create_user(profile) + if user: + user = db.session.query(User).filter_by( + username=profile['email'], auth_source=OAUTH2).first() + current_app.login_manager.logout_view = \ + OAuth2Authentication.LOGOUT_VIEW + return login_user(user), None + return False, msg + + def get_user_profile(self): + session['oauth2_token'] = self.oauth2_clients[ + self.oauth2_current_client].authorize_access_token() + + session['pass_enc_key'] = session['oauth2_token']['access_token'] + + resp = self.oauth2_clients[self.oauth2_current_client].get( + self.oauth2_config[ + self.oauth2_current_client]['OAUTH2_USERINFO_ENDPOINT'], + token=session['oauth2_token'] + ) + resp.raise_for_status() + return resp.json() + + def authenticate(self, form): + self.oauth2_current_client = request.form['oauth2_button'] + redirect_url = url_for(OAUTH2_AUTHORIZE, _external=True) + + if self.oauth2_current_client not in self.oauth2_clients: + return False, gettext( + "Please set the configuration parameters properly.") + return False, self.oauth2_clients[ + self.oauth2_current_client].authorize_redirect(redirect_url) + + def __auto_create_user(self, resp): + if config.OAUTH2_AUTO_CREATE_USER: + user = User.query.filter_by(username=resp['email'], + auth_source=OAUTH2).first() + if not user: + return create_user({ + 'username': resp['email'], + 'email': resp['email'], + 'role': 2, + 'active': True, + 'auth_source': OAUTH2 + }) + + return True, {'username': resp['email']} diff --git a/web/pgadmin/authenticate/static/js/kerberos.js b/web/pgadmin/authenticate/static/js/kerberos.js index 64373369c0d..8e4f2c50e65 100644 --- a/web/pgadmin/authenticate/static/js/kerberos.js +++ b/web/pgadmin/authenticate/static/js/kerberos.js @@ -1,10 +1,19 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + import url_for from 'sources/url_for'; import userInfo from 'pgadmin.user_management.current_user'; import pgConst from 'pgadmin.browser.constants'; function fetch_ticket() { // Fetch the Kerberos Updated ticket through SPNEGO - return fetch(url_for('authenticate.kerberos_update_ticket') + return fetch(url_for('kerberos.update_ticket') ) .then(function(response){ if (response.status >= 200 && response.status < 300) { @@ -18,7 +27,7 @@ function fetch_ticket() { function fetch_ticket_lifetime () { // Fetch the Kerberos ticket lifetime left - return fetch(url_for('authenticate.kerberos_validate_ticket') + return fetch(url_for('kerberos.validate_ticket') ) .then( function(response){ diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index fa7aa9e5487..d71a06e3d1f 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -50,7 +50,7 @@ set_crypt_key, process_masterpass_disabled from pgadmin.model import User from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\ - INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER + INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER, OAUTH2 from pgadmin.authenticate import AuthSourceManager try: @@ -607,14 +607,8 @@ def register_preferences(self): def _get_logout_url(): - if config.SERVER_MODE and\ - session['_auth_source_manager_obj']['current_source'] == \ - KERBEROS: - return '{0}?next={1}'.format(url_for( - 'authenticate.kerberos_logout'), url_for(BROWSER_INDEX)) - return '{0}?next={1}'.format( - url_for('security.logout'), url_for(BROWSER_INDEX)) + url_for(current_app.login_manager.logout_view), url_for(BROWSER_INDEX)) def _get_supported_browser(): @@ -748,10 +742,10 @@ def index(): if len(config.AUTHENTICATION_SOURCES) == 1\ and INTERNAL in config.AUTHENTICATION_SOURCES: auth_only_internal = True - auth_source = session['_auth_source_manager_obj'][ + auth_source = session['auth_source_manager'][ 'source_friendly_name'] - if session['_auth_source_manager_obj']['current_source'] == KERBEROS: + if not config.MASTER_PASSWORD_REQUIRED and 'pass_enc_key' in session: session['allow_save_password'] = False response = Response(render_template( @@ -877,7 +871,8 @@ def app_constants(): render_template('browser/js/constants.js', INTERNAL=INTERNAL, LDAP=LDAP, - KERBEROS=KERBEROS), + KERBEROS=KERBEROS, + OAUTH2=OAUTH2), 200, {'Content-Type': MIMETYPE_APP_JS} ) @@ -1005,8 +1000,9 @@ def set_master_password(): data = json.loads(data) # Master password is not applicable for server mode - if not config.SERVER_MODE and config.MASTER_PASSWORD_REQUIRED: - + # Enable master password if oauth is used + if not config.SERVER_MODE or OAUTH2 in config.AUTHENTICATION_SOURCES\ + and config.MASTER_PASSWORD_REQUIRED: # if master pass is set previously if current_user.masterpass_check is not None and \ data.get('button_click') and \ @@ -1043,7 +1039,7 @@ def set_master_password(): existing=True, present=False, ) - elif not get_crypt_key()[0]: + elif not get_crypt_key()[1]: error_message = None if data.get('button_click') and data.get('password') == '': # If user attempted to enter a blank password, then throw error @@ -1334,6 +1330,9 @@ def reset_password(token): do_flash(*get_message('PASSWORD_RESET')) login_user(user) + auth_obj = AuthSourceManager(form, [INTERNAL]) + session['auth_source_manager'] = auth_obj.as_dict() + return redirect(get_url(_security.post_reset_view) or get_url(_security.post_login_view)) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_role_dependencies_sql.py b/web/pgadmin/browser/server_groups/servers/tests/test_role_dependencies_sql.py index fefe6a75f6b..c186ac4f030 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/test_role_dependencies_sql.py +++ b/web/pgadmin/browser/server_groups/servers/tests/test_role_dependencies_sql.py @@ -7,6 +7,7 @@ # ########################################################################## import os +import uuid import jinja2 from regression.python_test_utils import test_utils @@ -24,20 +25,21 @@ class TestRoleDependenciesSql(SQLTemplateTestBase): def __init__(self): super(TestRoleDependenciesSql, self).__init__() self.table_id = -1 + self.role_name = "testpgadmin%s" % str(uuid.uuid4())[1:8] def setUp(self): with test_utils.Database(self.server) as (connection, database_name): cursor = connection.cursor() try: cursor.execute( - "CREATE ROLE testpgadmin LOGIN PASSWORD '%s'" - % self.server['db_password']) + "CREATE ROLE %s LOGIN PASSWORD '%s'" + % (self.role_name, self.server['db_password'])) except Exception as exception: print(exception) connection.commit() self.server_with_modified_user = self.server.copy() - self.server_with_modified_user['username'] = "testpgadmin" + self.server_with_modified_user['username'] = self.role_name def runTest(self): if hasattr(self, "ignore_test"): @@ -61,7 +63,7 @@ def runTest(self): def tearDown(self): with test_utils.Database(self.server) as (connection, database_name): cursor = connection.cursor() - cursor.execute("DROP ROLE testpgadmin") + cursor.execute("DROP ROLE %s" % self.role_name) connection.commit() def generate_sql(self, version): diff --git a/web/pgadmin/browser/templates/browser/js/constants.js b/web/pgadmin/browser/templates/browser/js/constants.js index 6a63d6ed9d6..6da835b6518 100644 --- a/web/pgadmin/browser/templates/browser/js/constants.js +++ b/web/pgadmin/browser/templates/browser/js/constants.js @@ -12,6 +12,7 @@ define('pgadmin.browser.constants', [], function() { return { 'INTERNAL': '{{ INTERNAL }}', 'LDAP': '{{ LDAP }}', - 'KERBEROS': '{{ KERBEROS }}' + 'KERBEROS': '{{ KERBEROS }}', + 'OAUTH2': '{{ OAUTH2 }}' } }); diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py index e67ced8c4df..0f49c444d31 100644 --- a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py +++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py @@ -13,6 +13,7 @@ from pgadmin.authenticate.registry import AuthSourceRegistry from unittest.mock import patch, MagicMock from werkzeug.datastructures import Headers +from pgadmin.utils.constants import LDAP, INTERNAL, KERBEROS class KerberosLoginMockTestCase(BaseTestGenerator): @@ -23,17 +24,17 @@ class KerberosLoginMockTestCase(BaseTestGenerator): scenarios = [ ('Spnego/Kerberos Authentication: Test Unauthorized', dict( - auth_source=['kerberos'], + auth_source=[KERBEROS], auto_create_user=True, flag=1 )), ('Spnego/Kerberos Authentication: Test Authorized', dict( - auth_source=['kerberos'], + auth_source=[KERBEROS], auto_create_user=True, flag=2 )), ('Spnego/Kerberos Update Ticket', dict( - auth_source=['kerberos'], + auth_source=[KERBEROS], auto_create_user=True, flag=3 )) @@ -49,7 +50,7 @@ def setUpClass(cls): def setUp(self): app_config.AUTHENTICATION_SOURCES = self.auth_source - self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'kerberos' + self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = KERBEROS def runTest(self): """This function checks spnego/kerberos login functionality.""" @@ -100,14 +101,13 @@ def __init__(self): self.initiator_name = 'user@PGADMIN.ORG' del_crads = delCrads() - - AuthSourceRegistry._registry['kerberos'].negotiate_start = MagicMock( + AuthSourceRegistry._registry[KERBEROS].negotiate_start = MagicMock( return_value=[True, del_crads]) return del_crads def test_update_ticket(self): # Response header should include the Negotiate header in the first call - response = self.tester.get('/authenticate/kerberos/update_ticket') + response = self.tester.get('/kerberos/update_ticket') self.assertEqual(response.status_code, 401) self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate') @@ -117,12 +117,13 @@ def test_update_ticket(self): krb_token = Headers({}) krb_token['Authorization'] = 'Negotiate CTOKEN' - response = self.tester.get('/authenticate/kerberos/update_ticket', + response = self.tester.get('/kerberos/update_ticket', headers=krb_token) self.assertEqual(response.status_code, 200) + self.tester.logout() def tearDown(self): - self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' + pass @classmethod def tearDownClass(cls): @@ -130,5 +131,6 @@ def tearDownClass(cls): We need to again login the test client as soon as test scenarios finishes. """ - app_config.AUTHENTICATION_SOURCES = ['internal'] + cls.tester.logout() + app_config.AUTHENTICATION_SOURCES = [INTERNAL] utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/browser/tests/test_ldap_login.py b/web/pgadmin/browser/tests/test_ldap_login.py index a21268f7f8d..51b512afd99 100644 --- a/web/pgadmin/browser/tests/test_ldap_login.py +++ b/web/pgadmin/browser/tests/test_ldap_login.py @@ -11,6 +11,7 @@ from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils import test_utils as utils from regression.test_setup import config_data +from pgadmin.utils.constants import LDAP, INTERNAL class LDAPLoginTestCase(BaseTestGenerator): @@ -50,7 +51,7 @@ def setUp(self): ldap_config = config_data['ldap_config'][0][self.config_key_param] except (KeyError, TypeError, IndexError): self.skipTest("LDAP config not set.") - app_config.AUTHENTICATION_SOURCES = ['ldap'] + app_config.AUTHENTICATION_SOURCES = [LDAP] app_config.LDAP_AUTO_CREATE_USER = True app_config.LDAP_SERVER_URI = ldap_config['uri'] app_config.LDAP_BASE_DN = ldap_config['base_dn'] @@ -70,6 +71,7 @@ def setUp(self): if ldap_config['anonymous_bind'] != "" and\ ldap_config['anonymous_bind']: app_config.LDAP_ANONYMOUS_BIND = True + self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP def runTest(self): """This function checks login functionality.""" @@ -92,5 +94,5 @@ def tearDownClass(cls): finishes. """ cls.tester.logout() - app_config.AUTHENTICATION_SOURCES = ['internal'] + app_config.AUTHENTICATION_SOURCES = [INTERNAL] utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/browser/tests/test_ldap_with_mocking.py b/web/pgadmin/browser/tests/test_ldap_with_mocking.py index 92f5c70c798..38c6b4724a1 100644 --- a/web/pgadmin/browser/tests/test_ldap_with_mocking.py +++ b/web/pgadmin/browser/tests/test_ldap_with_mocking.py @@ -13,6 +13,7 @@ from regression.test_setup import config_data from pgadmin.authenticate.registry import AuthSourceRegistry from unittest.mock import patch +from pgadmin.utils.constants import LDAP, INTERNAL class LDAPLoginMockTestCase(BaseTestGenerator): @@ -23,17 +24,17 @@ class LDAPLoginMockTestCase(BaseTestGenerator): scenarios = [ ('LDAP Authentication with Auto Create User', dict( - auth_source=['ldap'], + auth_source=[LDAP], auto_create_user=True, username='ldap_user', password='ldap_pass')), ('LDAP Authentication without Auto Create User', dict( - auth_source=['ldap'], + auth_source=[LDAP], auto_create_user=False, username='ldap_user', password='ldap_pass')), ('LDAP + Internal Authentication', dict( - auth_source=['ldap', 'internal'], + auth_source=[LDAP, INTERNAL], auto_create_user=False, username=config_data[ 'pgAdmin4_login_credentials']['login_username'], @@ -56,14 +57,15 @@ def setUp(self): app_config.LDAP_ANONYMOUS_BIND = False app_config.LDAP_BIND_USER = None app_config.LDAP_BIND_PASSWORD = None + self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP - @patch.object(AuthSourceRegistry._registry['ldap'], 'connect', + @patch.object(AuthSourceRegistry._registry[LDAP], 'connect', return_value=[True, "Done"]) - @patch.object(AuthSourceRegistry._registry['ldap'], 'search_ldap_user', + @patch.object(AuthSourceRegistry._registry[LDAP], 'search_ldap_user', return_value=[True, '']) def runTest(self, conn_mock_obj, search_mock_obj): """This function checks ldap login functionality.""" - AuthSourceRegistry._registry['ldap'].dedicated_user = False + AuthSourceRegistry._registry[LDAP].dedicated_user = False res = self.tester.login(self.username, self.password, True) respdata = 'Gravatar image for %s' % self.username self.assertTrue(respdata in res.data.decode('utf8')) @@ -78,5 +80,5 @@ def tearDownClass(cls): finishes. """ cls.tester.logout() - app_config.AUTHENTICATION_SOURCES = ['internal'] + app_config.AUTHENTICATION_SOURCES = [INTERNAL] utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/browser/tests/test_login.py b/web/pgadmin/browser/tests/test_login.py index 743d0099cdd..451c05b6461 100644 --- a/web/pgadmin/browser/tests/test_login.py +++ b/web/pgadmin/browser/tests/test_login.py @@ -12,6 +12,7 @@ from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils import test_utils as utils from regression.test_setup import config_data +from pgadmin.utils.constants import INTERNAL class LoginTestCase(BaseTestGenerator): @@ -98,7 +99,7 @@ def setUpClass(cls): # No need to call base class setup function def setUp(self): - pass + app_config.AUTHENTICATION_SOURCES = [INTERNAL] def runTest(self): """This function checks login functionality.""" diff --git a/web/pgadmin/browser/tests/test_master_password.py b/web/pgadmin/browser/tests/test_master_password.py index b5eabd6d0ff..be4675a73e9 100644 --- a/web/pgadmin/browser/tests/test_master_password.py +++ b/web/pgadmin/browser/tests/test_master_password.py @@ -11,6 +11,7 @@ from pgadmin.utils.route import BaseTestGenerator import config +from pgadmin.utils.constants import INTERNAL class MasterPasswordTestCase(BaseTestGenerator): @@ -53,6 +54,7 @@ class MasterPasswordTestCase(BaseTestGenerator): def setUp(self): config.MASTER_PASSWORD_REQUIRED = True + config.AUTHENTICATION_SOURCES = [INTERNAL] def runTest(self): """This function will check change password functionality.""" diff --git a/web/pgadmin/browser/tests/test_oauth2_with_mocking.py b/web/pgadmin/browser/tests/test_oauth2_with_mocking.py new file mode 100644 index 00000000000..b170720a8d4 --- /dev/null +++ b/web/pgadmin/browser/tests/test_oauth2_with_mocking.py @@ -0,0 +1,147 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2021, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from pgadmin.authenticate.registry import AuthSourceRegistry +from unittest.mock import patch, MagicMock +from pgadmin.authenticate import AuthSourceManager +from pgadmin.utils.constants import OAUTH2, LDAP, INTERNAL + + +class Oauth2LoginMockTestCase(BaseTestGenerator): + """ + This class checks oauth2 login functionality by mocking + External Oauth2 Authentication. + """ + + scenarios = [ + ('Oauth2 External Authentication', dict( + auth_source=['oauth2'], + oauth2_provider='github', + flag=1 + )), + ('Oauth2 Authentication', dict( + auth_source=['oauth2'], + oauth2_provider='github', + flag=2 + )), + ] + + @classmethod + def setUpClass(cls): + """ + We need to logout the test client as we are testing + spnego/kerberos login scenarios. + """ + cls.tester.logout() + + def setUp(self): + app_config.AUTHENTICATION_SOURCES = self.auth_source + self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = OAUTH2 + app_config.OAUTH2_CONFIG = [ + { + 'OAUTH2_NAME': 'github', + 'OAUTH2_DISPLAY_NAME': 'Github', + 'OAUTH2_CLIENT_ID': 'testclientid', + 'OAUTH2_CLIENT_SECRET': 'testclientsec', + 'OAUTH2_TOKEN_URL': + 'https://github.com/login/oauth/access_token', + 'OAUTH2_AUTHORIZATION_URL': + 'https://github.com/login/oauth/authorize', + 'OAUTH2_API_BASE_URL': 'https://api.github.com/', + 'OAUTH2_USERINFO_ENDPOINT': 'user', + 'OAUTH2_ICON': 'fa-github', + 'OAUTH2_BUTTON_COLOR': '#3253a8', + } + ] + + def runTest(self): + """This function checks oauth2 login functionality.""" + if app_config.SERVER_MODE is False: + self.skipTest( + "Can not run Oauth2 Authentication in the Desktop mode." + ) + + if self.flag == 1: + self.test_external_authentication() + elif self.flag == 2: + self.test_oauth2_authentication() + + def test_external_authentication(self): + """ + Ensure that the user should be redirected + to the external url for the authentication. + """ + + AuthSourceManager.update_auth_sources = MagicMock() + + try: + self.tester.login( + email=None, password=None, + _follow_redirects=True, + headers=None, + extra_form_data=dict(oauth2_button=self.oauth2_provider) + ) + except Exception as e: + self.assertEqual('Following external' + ' redirects is not supported.', str(e)) + + def test_oauth2_authentication(self): + """ + Ensure that when the client sends an correct authorization token, + they receive a 200 OK response and the user principal is extracted and + passed on to the routed method. + """ + + profile = self.mock_user_profile() + + # Mock Oauth2 Authenticate + AuthSourceRegistry._registry[OAUTH2].authenticate = MagicMock( + return_value=[True, '']) + + AuthSourceManager.update_auth_sources = MagicMock() + + # Create AuthSourceManager object + auth_obj = AuthSourceManager({}, [OAUTH2]) + auth_source = AuthSourceRegistry.get(OAUTH2) + auth_obj.set_source(auth_source) + auth_obj.set_current_source(auth_source.get_source_name()) + + # Check the login with Oauth2 + res = self.tester.login(email=None, password=None, + _follow_redirects=True, + headers=None, + extra_form_data=dict( + oauth2_button=self.oauth2_provider) + ) + + respdata = 'Gravatar image for %s' % profile['email'] + self.assertTrue(respdata in res.data.decode('utf8')) + + def mock_user_profile(self): + profile = {'email': 'oauth2@gmail.com'} + + AuthSourceRegistry._registry[OAUTH2].get_user_profile = MagicMock( + return_value=profile) + return profile + + def tearDown(self): + self.tester.logout() + + @classmethod + def tearDownClass(cls): + """ + We need to again login the test client as soon as test scenarios + finishes. + """ + cls.tester.logout() + app_config.AUTHENTICATION_SOURCES = [INTERNAL] + utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py index 25e0a2a9eb0..4da7896a032 100644 --- a/web/pgadmin/misc/bgprocess/processes.py +++ b/web/pgadmin/misc/bgprocess/processes.py @@ -280,7 +280,7 @@ def start(self, cb=None): env['OUTDIR'] = self.log_dir env['PGA_BGP_FOREGROUND'] = "1" if config.SERVER_MODE and session and \ - session['_auth_source_manager_obj']['current_source'] == \ + session['auth_source_manager']['current_source'] == \ KERBEROS: env['KRB5CCNAME'] = session['KRB5CCNAME'] diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss index 6a185471b31..d990b550988 100644 --- a/web/pgadmin/static/scss/_pgadmin.style.scss +++ b/web/pgadmin/static/scss/_pgadmin.style.scss @@ -946,6 +946,9 @@ table.table-empty-rows{ & .btn-login { background-color: $security-btn-color; } + & .btn-oauth { + background-color: $security-btn-color; + } & .user-language { & select{ background-color: $color-primary; diff --git a/web/pgadmin/templates/security/login_user.html b/web/pgadmin/templates/security/login_user.html index 2e92d7b125e..884562d2188 100644 --- a/web/pgadmin/templates/security/login_user.html +++ b/web/pgadmin/templates/security/login_user.html @@ -12,7 +12,7 @@ {% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %} {{ render_username_with_errors(login_user_form.email, "text") }} {{ render_field_with_errors(login_user_form.password, "password") }} - +
{{ _('Forgotten your password?', url=url_for('browser.forgot_password')) }}
@@ -20,9 +20,16 @@ {% for key, lang in config.LANGUAGES.items() %} {% endfor %} - +
+{% if config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.AUTHENTICATION_SOURCES %} + {% for oauth_config in config.OAUTH2_CONFIG %} + + {% endfor %} +{% endif %} {% endif %} {% endblock %} diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py index 5d56081bad9..3aabd774989 100644 --- a/web/pgadmin/tools/user_management/__init__.py +++ b/web/pgadmin/tools/user_management/__init__.py @@ -176,7 +176,7 @@ def current_user_info(): config.ALLOW_SAVE_TUNNEL_PASSWORD and session[ 'allow_save_password'] else 'false', auth_sources=config.AUTHENTICATION_SOURCES, - current_auth_source=session['_auth_source_manager_obj'][ + current_auth_source=session['auth_source_manager'][ 'current_source'] if config.SERVER_MODE is True else INTERNAL ), status=200, diff --git a/web/pgadmin/tools/user_management/static/js/user_management.js b/web/pgadmin/tools/user_management/static/js/user_management.js index e436f494893..ffed1d44df6 100644 --- a/web/pgadmin/tools/user_management/static/js/user_management.js +++ b/web/pgadmin/tools/user_management/static/js/user_management.js @@ -28,6 +28,7 @@ define([ DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'], LDAP = pgConst['LDAP'], KERBEROS = pgConst['KERBEROS'], + OAUTH2 = pgConst['OAUTH2'], AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false, userFilter = function(collection) { return (new Backgrid.Extension.ClientSideFilter({ @@ -607,6 +608,16 @@ define([ this.get('username') ); + this.errorModel.set('username', errmsg); + return errmsg; + } + else if (!!this.get('username') && this.collection.nonFilter.where({ + 'username': this.get('username'), 'auth_source': OAUTH2, + }).length > 1) { + errmsg = gettext('The username %s already exists.', + this.get('username') + ); + this.errorModel.set('username', errmsg); return errmsg; } @@ -1053,7 +1064,7 @@ define([ saveUser: function(m) { var d = m.toJSON(true); - if((m.isNew() && (m.get('auth_source') == LDAP || m.get('auth_source') == KERBEROS) && (!m.get('username') || !m.get('auth_source') || !m.get('role'))) + if((m.isNew() && (m.get('auth_source') == LDAP || m.get('auth_source') == KERBEROS || m.get('auth_source') == OAUTH2) && (!m.get('username') || !m.get('auth_source') || !m.get('role'))) || (m.isNew() && m.get('auth_source') == DEFAULT_AUTH_SOURCE && (!m.get('email') || !m.get('role') || !m.get('newPassword') || !m.get('confirmPassword') || m.get('newPassword') != m.get('confirmPassword'))) || (!m.isNew() && m.get('newPassword') != m.get('confirmPassword'))) { diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 67cc690e983..bee5bbffb26 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -55,10 +55,12 @@ INTERNAL = 'internal' LDAP = 'ldap' KERBEROS = 'kerberos' +OAUTH2 = "oauth2" SUPPORTED_AUTH_SOURCES = [INTERNAL, LDAP, - KERBEROS] + KERBEROS, + OAUTH2] BINARY_PATHS = { "as_bin_paths": [ diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index 3bddb9fa54f..69512427358 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -319,7 +319,7 @@ def connect(self, **kwargs): config.APP_NAME, conn_id) if config.SERVER_MODE and \ - session['_auth_source_manager_obj']['current_source'] == \ + session['auth_source_manager']['current_source'] == \ KERBEROS and 'KRB5CCNAME' in session\ and manager.kerberos_conn: lock.acquire() @@ -353,7 +353,7 @@ def connect(self, **kwargs): self._wait(pg_conn) if config.SERVER_MODE and \ - session['_auth_source_manager_obj']['current_source'] == \ + session['auth_source_manager']['current_source'] == \ KERBEROS: environ['KRB5CCNAME'] = '' @@ -378,7 +378,7 @@ def connect(self, **kwargs): return False, msg finally: if config.SERVER_MODE and \ - session['_auth_source_manager_obj']['current_source'] == \ + session['auth_source_manager']['current_source'] == \ KERBEROS and lock.locked(): lock.release() diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py index f962684ffd1..fcf15af53cd 100644 --- a/web/pgadmin/utils/master_password.py +++ b/web/pgadmin/utils/master_password.py @@ -31,13 +31,11 @@ def get_crypt_key(): return True, current_user.password # if desktop mode and master pass enabled elif config.MASTER_PASSWORD_REQUIRED \ - and not config.SERVER_MODE and enc_key is None: + and enc_key is None: return False, None - elif config.SERVER_MODE and \ - session['_auth_source_manager_obj']['current_source']\ - == KERBEROS: - return True, session['kerberos_key'] if 'kerberos_key' in session \ - else None + elif not config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \ + 'pass_enc_key' in session: + return True, session['pass_enc_key'] else: return True, enc_key diff --git a/web/regression/python_test_utils/csrf_test_client.py b/web/regression/python_test_utils/csrf_test_client.py index 5e52590147d..78189f238fa 100644 --- a/web/regression/python_test_utils/csrf_test_client.py +++ b/web/regression/python_test_utils/csrf_test_client.py @@ -92,9 +92,7 @@ def generate_csrf_token(self, *args, **kwargs): # and make a test request context that has those cookies in it. environ_overrides = {} self.cookie_jar.inject_wsgi(environ_overrides) - with self.app.test_request_context( - "/login", environ_overrides=environ_overrides, - ): + with self.app.test_request_context(): # Now, we call Flask-WTF's method of generating a CSRF token... csrf_token = generate_csrf() # ...which also sets a value in `flask.session`, so we need to @@ -106,18 +104,27 @@ def generate_csrf_token(self, *args, **kwargs): return csrf_token def login(self, email, password, _follow_redirects=False, - headers=None): + headers=None, extra_form_data=dict()): + csrf_token = None if config.SERVER_MODE is True: - res = self.get('/login', follow_redirects=True) + res = self.get('/login', + follow_redirects=_follow_redirects) csrf_token = self.fetch_csrf(res) - else: + + if csrf_token is None: csrf_token = self.generate_csrf_token() + form_data = dict( + email=email, + password=password, + csrf_token=csrf_token + ) + + if extra_form_data: + form_data.update(extra_form_data) + res = self.post( - '/authenticate/login', data=dict( - email=email, password=password, - csrf_token=csrf_token, - ), + '/authenticate/login', data=form_data, follow_redirects=_follow_redirects, headers=headers ) diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index 9dba4cf063b..d8b92dec6e5 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -1656,14 +1656,14 @@ def create_user(user_details): cur = conn.cursor() user_details = ( user_details['login_username'], user_details['login_username'], - user_details['login_password'], 1) + user_details['login_password'], 1, uuid.uuid4().hex) cur.execute( 'select * from user where username = "%s"' % user_details[0]) user = cur.fetchone() if user is None: - cur.execute('INSERT INTO user (username, email, password, active) ' - 'VALUES (?,?,?,?)', user_details) + cur.execute('INSERT INTO user (username, email, password, active,' + ' fs_uniquifier) VALUES (?,?,?,?,?)', user_details) user_id = cur.lastrowid conn.commit() else: diff --git a/web/regression/runtests.py b/web/regression/runtests.py index cea20e3cce9..7503f7c639d 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -97,6 +97,7 @@ from regression.python_test_utils.csrf_test_client import TestClient config.SETTINGS_SCHEMA_VERSION = SCHEMA_VERSION +from pgadmin.utils.constants import LDAP # Override some other defaults from logging import WARNING @@ -117,7 +118,7 @@ app.config['WTF_CSRF_ENABLED'] = True # Authentication sources -app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' +app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP app.test_client_class = TestClient test_client = app.test_client() diff --git a/web/yarn.lock b/web/yarn.lock index cd614283b82..5e1e721756b 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -9016,9 +9016,9 @@ watchpack@^2.0.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -"webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f": +"webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#06daee1a8111b4ed08d58670674a2967e4f68313": version "2.2.5" - resolved "git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f" + resolved "git+https://github.com/EnterpriseDB/wcDocker/#06daee1a8111b4ed08d58670674a2967e4f68313" dependencies: "@fortawesome/fontawesome-free" "^5.14.0" FileSaver "^0.10.0"