diff --git a/docs/en_US/release_notes_4_30.rst b/docs/en_US/release_notes_4_30.rst index d93250336..f8a3f96d1 100644 --- a/docs/en_US/release_notes_4_30.rst +++ b/docs/en_US/release_notes_4_30.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #5457 `_ - Added support for Kerberos authentication, using SPNEGO to forward the Kerberos tickets through a browser. Housekeeping ************ @@ -23,6 +24,7 @@ Bug fixes | `Issue #5282 `_ - Added 'Count Rows' option to the partition sub tables. | `Issue #5488 `_ - Improve the explain plan details by showing popup instead of tooltip on clicking of the specified node. | `Issue #5571 `_ - Added support for expression in exclusion constraints. +| `Issue #5829 `_ - Fixed incorrect log information for AUTHENTICATION_SOURCES. | `Issue #5875 `_ - Ensure that the 'template1' database should not be visible after pg_upgrade. | `Issue #5965 `_ - Ensure that the macro query result should be download properly. | `Issue #5973 `_ - Added appropriate help message and a placeholder for letting users know about the account password expiry for Login/Group Role. diff --git a/requirements.txt b/requirements.txt index f391c08df..25acb5332 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,3 +43,4 @@ cryptography<=3.0; sshtunnel>=0.1.5 ldap3>=2.5.1 Flask-BabelEx>=0.9.4 +gssapi>=1.6.11 diff --git a/web/config.py b/web/config.py index 2b314fe69..d02a91380 100644 --- a/web/config.py +++ b/web/config.py @@ -535,7 +535,7 @@ ENHANCED_COOKIE_PROTECTION = True ########################################################################## # Default setting is internal -# External Supported Sources: ldap +# External Supported Sources: ldap, kerberos # 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. @@ -618,6 +618,26 @@ LDAP_CA_CERT_FILE = '' LDAP_CERT_FILE = '' LDAP_KEY_FILE = '' + +########################################################################## +# Kerberos Configuration +########################################################################## + +KRB_APP_HOST_NAME = DEFAULT_SERVER + +# If the default_keytab_name is not set in krb5.conf or +# the KRB_KTNAME environment variable is not set then, explicitly set +# the Keytab file + +KRB_KTNAME = '' + +# After kerberos 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. + +KRB_AUTO_CREATE_USER = True + ########################################################################## # Local config settings ########################################################################## diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index ff9c00f50..14afe7dc1 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -35,6 +35,9 @@ else: import config from pgadmin import create_app from pgadmin.utils import u_encode, fs_encoding, file_quote +from pgadmin.utils.constants import INTERNAL, LDAP,\ + KERBEROS, SUPPORTED_AUTH_SOURCES + # Get the config database schema version. We store this in pgadmin.model # as it turns out that putting it in the config files isn't a great idea from pgadmin.model import SCHEMA_VERSION @@ -96,15 +99,11 @@ if config.SERVER_MODE: app.wsgi_app = ReverseProxied(app.wsgi_app) # Authentication sources -app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal' -app.PGADMIN_SUPPORTED_AUTH_SOURCE = ['internal', 'ldap'] + if len(config.AUTHENTICATION_SOURCES) > 0: app.PGADMIN_EXTERNAL_AUTH_SOURCE = config.AUTHENTICATION_SOURCES[0] else: - app.PGADMIN_EXTERNAL_AUTH_SOURCE = app.PGADMIN_DEFAULT_AUTH_SOURCE - -app.logger.debug( - "Authentication Source: %s" % app.PGADMIN_DEFAULT_AUTH_SOURCE) + app.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL # Start the web server. The port number should have already been set by the # runtime if we're running in desktop mode, otherwise we'll just use the diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index dae0b8cd2..cdbef9812 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -43,6 +43,7 @@ from pgadmin.utils.ajax import internal_server_error, make_json_response from pgadmin.utils.csrf import pgCSRFProtect from pgadmin import authenticate from pgadmin.utils.security_headers import SecurityHeaders +from pgadmin.utils.constants import KERBEROS # 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 @@ -695,11 +696,19 @@ def create_app(app_name=None): ) abort(401) login_user(user) + elif config.SERVER_MODE and\ + app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\ + KERBEROS and \ + not current_user.is_authenticated and \ + request.endpoint in ('redirects.index', 'security.login'): + 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 \ 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 7ede73cd8..1fdb66cf7 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -11,16 +11,21 @@ import flask import pickle -from flask import current_app, flash +from flask import current_app, flash, Response, request, url_for,\ + render_template from flask_babelex import gettext from flask_security import current_user from flask_security.views import _security, _ctx from flask_security.utils import config_value, get_post_logout_redirect, \ - get_post_login_redirect + get_post_login_redirect, logout_user + from flask import session import config from pgadmin.utils import PgAdminModule +from pgadmin.utils.constants import KERBEROS +from pgadmin.utils.csrf import pgCSRFProtect + from .registry import AuthSourceRegistry MODULE_NAME = 'authenticate' @@ -28,12 +33,34 @@ MODULE_NAME = 'authenticate' class AuthenticateModule(PgAdminModule): def get_exposed_url_endpoints(self): - return ['authenticate.login'] + return ['authenticate.login', + 'authenticate.kerberos_login', + 'authenticate.kerberos_logout'] 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() + return Response(render_template("browser/kerberos_logout.html", + login_url=url_for('security.login'), + )) + + @blueprint.route('/login', endpoint='login', methods=['GET', 'POST']) def login(): """ @@ -56,15 +83,24 @@ def login(): if status: # 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( + 'authenticate.kerberos_login'), url_for('browser.index'))) + flash(gettext(msg), 'danger') return flask.redirect(get_post_logout_redirect()) - session['_auth_source_manager_obj'] = auth_obj.as_dict() + session['_auth_source_manager_obj'] = current_auth_obj return flask.redirect(get_post_login_redirect()) + elif isinstance(msg, Response): + return msg flash(gettext(msg), 'danger') - return flask.redirect(get_post_logout_redirect()) + response = flask.redirect(get_post_logout_redirect()) + return response class AuthSourceManager(): @@ -75,6 +111,7 @@ class AuthSourceManager(): self.auth_sources = sources self.source = None self.source_friendly_name = None + self.current_source = None def as_dict(self): """ @@ -84,9 +121,17 @@ class AuthSourceManager(): res = dict() res['source_friendly_name'] = self.source_friendly_name res['auth_sources'] = self.auth_sources + res['current_source'] = self.current_source return res + def set_current_source(self, source): + self.current_source = source + + @property + def get_current_source(self, source): + return self.current_source + def set_source(self, source): self.source = source @@ -115,9 +160,33 @@ class AuthSourceManager(): msg = None for src in self.auth_sources: source = get_auth_sources(src) + 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: + 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 @@ -125,6 +194,9 @@ class AuthSourceManager(): status, msg = self.source.login(self.form) if status: self.set_source_friendly_name(self.source.get_friendly_name()) + current_app.logger.debug( + "Authentication and Login successfully done via source : %s" % + self.source.get_source_name()) return status, msg diff --git a/web/pgadmin/authenticate/internal.py b/web/pgadmin/authenticate/internal.py index 804a487c7..484a7fdca 100644 --- a/web/pgadmin/authenticate/internal.py +++ b/web/pgadmin/authenticate/internal.py @@ -18,6 +18,7 @@ from flask_babelex import gettext from .registry import AuthSourceRegistry from pgadmin.model import User from pgadmin.utils.validation_utils import validate_email +from pgadmin.utils.constants import INTERNAL @six.add_metaclass(AuthSourceRegistry) @@ -31,7 +32,11 @@ class BaseAuthentication(object): 'INVALID_EMAIL': gettext('Email/Username is not valid') } - @abstractproperty + @abstractmethod + def get_source_name(self): + pass + + @abstractmethod def get_friendly_name(self): pass @@ -82,6 +87,9 @@ class BaseAuthentication(object): class InternalAuthentication(BaseAuthentication): + def get_source_name(self): + return INTERNAL + def get_friendly_name(self): return gettext("internal") diff --git a/web/pgadmin/authenticate/kerberos.py b/web/pgadmin/authenticate/kerberos.py new file mode 100644 index 000000000..dc3ffdc08 --- /dev/null +++ b/web/pgadmin/authenticate/kerberos.py @@ -0,0 +1,138 @@ +########################################################################## +# +# 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 Spnego/Kerberos authentication.""" + +import base64 +import gssapi +from os import environ + +from werkzeug.datastructures import Headers +from flask_babelex import gettext +from flask import Flask, request, Response, session,\ + current_app, render_template, flash + +import config +from pgadmin.model import User, ServerGroup, db, Role +from pgadmin.tools.user_management import create_user +from pgadmin.utils.constants import KERBEROS + +from flask_security.views import _security, _commit, _ctx +from werkzeug.datastructures import MultiDict + +from .internal import BaseAuthentication + + +# Set the Kerberos config file +if config.KRB_KTNAME and config.KRB_KTNAME != '': + environ['KRB5_KTNAME'] = config.KRB_KTNAME + + +class KerberosAuthentication(BaseAuthentication): + + def get_source_name(self): + return KERBEROS + + def get_friendly_name(self): + return gettext("kerberos") + + def validate(self, form): + return True + + def authenticate(self, frm): + retval = [True, None] + negotiate = False + headers = Headers() + authorization = request.headers.get("Authorization", None) + form_class = _security.login_form + + if request.json: + form = form_class(MultiDict(request.json)) + else: + form = form_class() + + try: + if authorization is not None: + auth_header = authorization.split() + if auth_header[0] == 'Negotiate': + status, negotiate = self.negotiate_start(auth_header[1]) + + if status: + # Saving the first 15 characters of the kerberos key + # to encrypt/decrypt database password + session['kerberos_key'] = auth_header[1][0:15] + # Create user + retval = self.__auto_create_user( + str(negotiate.initiator_name)) + elif isinstance(negotiate, Exception): + flash(gettext(negotiate), 'danger') + retval = [status, + Response(render_template( + "security/login_user.html", + login_user_form=form))] + else: + headers.add('WWW-Authenticate', 'Negotiate ' + + str(base64.b64encode(negotiate), 'utf-8')) + return False, Response("Success", 200, headers) + else: + flash(gettext("Kerberos authentication failed." + " Couldn't find kerberos ticket."), 'danger') + headers.add('WWW-Authenticate', 'Negotiate') + retval = [False, + Response(render_template( + "security/login_user.html", + login_user_form=form), 401, headers)] + finally: + if negotiate is not False: + self.negotiate_end(negotiate) + return retval + + def negotiate_start(self, in_token): + svc_princ = gssapi.Name('HTTP@%s' % config.KRB_APP_HOST_NAME, + name_type=gssapi.NameType.hostbased_service) + cname = svc_princ.canonicalize(gssapi.MechType.kerberos) + + try: + server_creds = gssapi.Credentials(usage='accept', name=cname) + context = gssapi.SecurityContext(creds=server_creds) + out_token = context.step(base64.b64decode(in_token)) + except Exception as e: + current_app.logger.exception(e) + return False, e + + if out_token and not context.complete: + return False, out_token + if context.complete: + return True, context + else: + return False, None + + def negotiate_end(self, context): + # Free gss_cred_id_t + del_creds = getattr(context, 'delegated_creds', None) + if del_creds: + deleg_creds = context.delegated_creds + del(deleg_creds) + + def __auto_create_user(self, username): + """Add the ldap user to the internal SQLite database.""" + username = str(username) + if config.KRB_AUTO_CREATE_USER: + user = User.query.filter_by( + username=username).first() + if user is None: + return create_user({ + 'username': username, + 'email': username, + 'role': 2, + 'active': True, + 'auth_source': KERBEROS + }) + + return True, {'username': username} diff --git a/web/pgadmin/authenticate/ldap.py b/web/pgadmin/authenticate/ldap.py index a9eca110f..2f0f61b7c 100644 --- a/web/pgadmin/authenticate/ldap.py +++ b/web/pgadmin/authenticate/ldap.py @@ -23,6 +23,7 @@ from .internal import BaseAuthentication from pgadmin.model import User, ServerGroup, db, Role from flask import current_app from pgadmin.tools.user_management import create_user +from pgadmin.utils.constants import LDAP ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}" @@ -31,6 +32,9 @@ ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}" class LDAPAuthentication(BaseAuthentication): """Ldap Authentication Class""" + def get_source_name(self): + return LDAP + def get_friendly_name(self): return gettext("ldap") @@ -151,7 +155,7 @@ class LDAPAuthentication(BaseAuthentication): 'email': user_email, 'role': 2, 'active': True, - 'auth_source': 'ldap' + 'auth_source': LDAP }) return True, None diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 1bae28f9c..c0ad869a1 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -29,7 +29,7 @@ from flask_security.recoverable import reset_password_token_status, \ generate_reset_password_token, update_password from flask_security.signals import reset_password_instructions_sent from flask_security.utils import config_value, do_flash, get_url, \ - get_message, slash_url_suffix, login_user, send_mail + get_message, slash_url_suffix, login_user, send_mail, logout_user from flask_security.views import _security, _commit, _ctx from werkzeug.datastructures import MultiDict @@ -47,7 +47,8 @@ from pgadmin.utils.master_password import validate_master_password, \ set_masterpass_check_text, cleanup_master_password, get_crypt_key, \ set_crypt_key, process_masterpass_disabled from pgadmin.model import User -from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE +from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\ + INTERNAL, KERBEROS try: from flask_security.views import default_render_json @@ -280,7 +281,8 @@ class BrowserModule(PgAdminModule): 'browser.check_master_password', 'browser.set_master_password', 'browser.reset_master_password', - 'browser.lock_layout'] + 'browser.lock_layout' + ] blueprint = BrowserModule(MODULE_NAME, __name__) @@ -539,6 +541,12 @@ class BrowserPluginModule(PgAdminModule): 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)) @@ -664,13 +672,18 @@ def index(): auth_only_internal = False auth_source = [] + session['allow_save_password'] = True + if config.SERVER_MODE: if len(config.AUTHENTICATION_SOURCES) == 1\ - and 'internal' in config.AUTHENTICATION_SOURCES: + and INTERNAL in config.AUTHENTICATION_SOURCES: auth_only_internal = True auth_source = session['_auth_source_manager_obj'][ 'source_friendly_name'] + if session['_auth_source_manager_obj']['current_source'] == KERBEROS: + session['allow_save_password'] = False + response = Response(render_template( MODULE_NAME + "/index.html", username=current_user.username, @@ -1086,7 +1099,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: # Check the Authentication source of the User user = User.query.filter_by( email=form.data['email'], - auth_source=current_app.PGADMIN_DEFAULT_AUTH_SOURCE + auth_source=INTERNAL ).first() if user is None: diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index ecc1281a2..5daef8120 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -10,7 +10,7 @@ import simplejson as json import pgadmin.browser.server_groups as sg from flask import render_template, request, make_response, jsonify, \ - current_app, url_for + current_app, url_for, session from flask_babelex import gettext from flask_security import current_user, login_required from pgadmin.browser.server_groups.servers.types import ServerType @@ -1822,7 +1822,13 @@ class ServerNode(PGChildNodeView): _=gettext, service=server.service, prompt_tunnel_password=prompt_tunnel_password, - prompt_password=prompt_password + prompt_password=prompt_password, + allow_save_password=True if + config.ALLOW_SAVE_PASSWORD and + session['allow_save_password'] else False, + allow_save_tunnel_password=True if + config.ALLOW_SAVE_TUNNEL_PASSWORD and + session['allow_save_password'] else False ) ) else: @@ -1836,6 +1842,9 @@ class ServerNode(PGChildNodeView): errmsg=errmsg, service=server.service, _=gettext, + allow_save_password=True if + config.ALLOW_SAVE_PASSWORD and + session['allow_save_password'] else False, ) ) diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/password.html b/web/pgadmin/browser/server_groups/servers/templates/servers/password.html index 9b2c425e3..35f4e2a16 100644 --- a/web/pgadmin/browser/server_groups/servers/templates/servers/password.html +++ b/web/pgadmin/browser/server_groups/servers/templates/servers/password.html @@ -19,7 +19,7 @@
diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html b/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html index 5de642f85..e34a257f2 100644 --- a/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html +++ b/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html @@ -15,7 +15,7 @@
@@ -39,7 +39,7 @@
diff --git a/web/pgadmin/browser/templates/browser/kerberos_login.html b/web/pgadmin/browser/templates/browser/kerberos_login.html new file mode 100644 index 000000000..c112e3196 --- /dev/null +++ b/web/pgadmin/browser/templates/browser/kerberos_login.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block body %} +
+
+
+
+
{{ _('%(appname)s', appname=config.APP_NAME) }}
+
+
{{ _('Login Failed.') }}
+
Click here to Login again.
+
+
+
+
+
+{% endblock %} diff --git a/web/pgadmin/browser/templates/browser/kerberos_logout.html b/web/pgadmin/browser/templates/browser/kerberos_logout.html new file mode 100644 index 000000000..430dc6f25 --- /dev/null +++ b/web/pgadmin/browser/templates/browser/kerberos_logout.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block body %} +
+
+
+
+
{{ _('%(appname)s', appname=config.APP_NAME) }}
+
+
{{ _('Logged out successfully.') }}
+
Click here to Login again.
+
+
+
+
+
+{% endblock %} diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py new file mode 100644 index 000000000..eaa984fb9 --- /dev/null +++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py @@ -0,0 +1,104 @@ +########################################################################## +# +# 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 + + +class KerberosLoginMockTestCase(BaseTestGenerator): + """ + This class checks Spnego/Kerberos login functionality by mocking + HTTP negotiate authentication. + """ + + scenarios = [ + ('Spnego/Kerberos Authentication: Test Unauthorized', dict( + auth_source=['kerberos'], + auto_create_user=True, + flag=1 + )), + ('Spnego/Kerberos Authentication: Test Authorized', dict( + auth_source=['kerberos'], + auto_create_user=True, + 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 = 'kerberos' + + def runTest(self): + """This function checks spnego/kerberos login functionality.""" + if self.flag == 1: + self.test_unauthorized() + elif self.flag == 2: + if app_config.SERVER_MODE is False: + self.skipTest( + "Can not run Kerberos Authentication in the Desktop mode." + ) + + self.test_authorized() + + def test_unauthorized(self): + """ + Ensure that when client sends the first request, + the Negotiate request is sent. + """ + res = self.tester.login(None, None, True) + self.assertEqual(res.status_code, 401) + self.assertEqual(res.headers.get('www-authenticate'), 'Negotiate') + + def test_authorized(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. + """ + + class delCrads: + def __init__(self): + self.initiator_name = 'user@PGADMIN.ORG' + del_crads = delCrads() + + AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock( + return_value=[True, del_crads]) + res = self.tester.login(None, + None, + True, + headers={'Authorization': 'Negotiate CTOKEN'} + ) + self.assertEqual(res.status_code, 200) + respdata = 'Gravatar image for %s' % del_crads.initiator_name + self.assertTrue(respdata in res.data.decode('utf8')) + + def tearDown(self): + self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' + 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/tools/datagrid/__init__.py b/web/pgadmin/tools/datagrid/__init__.py index 2405a498d..05ed998c6 100644 --- a/web/pgadmin/tools/datagrid/__init__.py +++ b/web/pgadmin/tools/datagrid/__init__.py @@ -25,7 +25,7 @@ from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response, bad_request, \ internal_server_error, unauthorized -from config import PG_DEFAULT_DRIVER +from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD from pgadmin.model import Server, User from pgadmin.utils.driver import get_driver from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost @@ -402,6 +402,9 @@ def _init_query_tool(trans_id, connect, sgid, sid, did, **kwargs): username=user, errmsg=msg, _=gettext, + allow_save_password=True if + ALLOW_SAVE_PASSWORD and + session['allow_save_password'] else False, ) ), '', '' else: diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py index 8641130c4..ce280a3d2 100644 --- a/web/pgadmin/tools/user_management/__init__.py +++ b/web/pgadmin/tools/user_management/__init__.py @@ -13,7 +13,7 @@ import simplejson as json import re from flask import render_template, request, \ - url_for, Response, abort, current_app + url_for, Response, abort, current_app, session from flask_babelex import gettext as _ from flask_security import login_required, roles_required, current_user from flask_security.utils import encrypt_password @@ -24,7 +24,8 @@ from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_response as ajax_response, \ make_json_response, bad_request, internal_server_error, forbidden from pgadmin.utils.csrf import pgCSRFProtect -from pgadmin.utils.constants import MIMETYPE_APP_JS +from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\ + SUPPORTED_AUTH_SOURCES, KERBEROS from pgadmin.utils.validation_utils import validate_email from pgadmin.model import db, Role, User, UserPreference, Server, \ ServerGroup, Process, Setting @@ -167,11 +168,13 @@ def current_user_info(): config.SERVER_MODE is True else 'postgres' ), - allow_save_password='true' if config.ALLOW_SAVE_PASSWORD + allow_save_password='true' if + config.ALLOW_SAVE_PASSWORD and session['allow_save_password'] else 'false', - allow_save_tunnel_password='true' - if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false', - auth_sources=config.AUTHENTICATION_SOURCES, + allow_save_tunnel_password='true' if + config.ALLOW_SAVE_TUNNEL_PASSWORD and session[ + 'allow_save_password'] else 'false', + auth_sources=config.AUTHENTICATION_SOURCES ), status=200, mimetype=MIMETYPE_APP_JS @@ -254,10 +257,10 @@ def _create_new_user(new_data): :return: Return new created user. """ auth_source = new_data['auth_source'] if 'auth_source' in new_data \ - else current_app.PGADMIN_DEFAULT_AUTH_SOURCE + else INTERNAL username = new_data['username'] if \ 'username' in new_data and auth_source != \ - current_app.PGADMIN_DEFAULT_AUTH_SOURCE else new_data['email'] + INTERNAL else new_data['email'] email = new_data['email'] if 'email' in new_data else None password = new_data['password'] if 'password' in new_data else None @@ -279,7 +282,7 @@ def _create_new_user(new_data): def create_user(data): if 'auth_source' in data and data['auth_source'] != \ - current_app.PGADMIN_DEFAULT_AUTH_SOURCE: + INTERNAL: req_params = ('username', 'role', 'active', 'auth_source') else: req_params = ('email', 'role', 'active', 'newPassword', @@ -380,7 +383,7 @@ def update(uid): ) # Username and email can not be changed for internal users - if usr.auth_source == current_app.PGADMIN_DEFAULT_AUTH_SOURCE: + if usr.auth_source == INTERNAL: non_editable_params = ('username', 'email') for f in non_editable_params: @@ -463,7 +466,7 @@ def role(rid): ) def auth_sources(): sources = [] - for source in current_app.PGADMIN_SUPPORTED_AUTH_SOURCE: + for source in SUPPORTED_AUTH_SOURCES: sources.append({'label': source, 'value': source}) return ajax_response( diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 0a2261f05..5fd942304 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -47,3 +47,12 @@ ERROR_FETCHING_ROLE_INFORMATION = gettext( 'Error fetching role information from the database server.') ERROR_FETCHING_DATA = gettext('Unable to fetch data.') + +# Authentication Sources +INTERNAL = 'internal' +LDAP = 'ldap' +KERBEROS = 'kerberos' + +SUPPORTED_AUTH_SOURCES = [INTERNAL, + LDAP, + KERBEROS] diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py index 759bf36e0..629eec941 100644 --- a/web/pgadmin/utils/master_password.py +++ b/web/pgadmin/utils/master_password.py @@ -1,8 +1,9 @@ import config -from flask import current_app +from flask import current_app, session from flask_login import current_user from pgadmin.model import db, User, Server from pgadmin.utils.crypto import encrypt, decrypt +from pgadmin.utils.constants import KERBEROS MASTERPASS_CHECK_TEXT = 'ideas are bulletproof' @@ -32,6 +33,11 @@ def get_crypt_key(): elif config.MASTER_PASSWORD_REQUIRED \ and not config.SERVER_MODE and enc_key is None: return False, None + elif config.SERVER_MODE and \ + session['_auth_source_manager_obj']['source_friendly_name']\ + == KERBEROS: + return True, session['kerberos_key'] if 'kerberos_key' in session \ + else None 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 11d2cfca5..ca4120e18 100644 --- a/web/regression/python_test_utils/csrf_test_client.py +++ b/web/regression/python_test_utils/csrf_test_client.py @@ -101,7 +101,8 @@ class TestClient(testing.FlaskClient): return csrf_token - def login(self, email, password, _follow_redirects=False): + def login(self, email, password, _follow_redirects=False, + headers=None): if config.SERVER_MODE is True: res = self.get('/login', follow_redirects=True) csrf_token = self.fetch_csrf(res) @@ -113,7 +114,8 @@ class TestClient(testing.FlaskClient): email=email, password=password, csrf_token=csrf_token, ), - follow_redirects=_follow_redirects + follow_redirects=_follow_redirects, + headers=headers ) self.csrf_token = csrf_token diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 3328ed3f6..9b794e41f 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -117,9 +117,9 @@ if config.SERVER_MODE is True: app.config['WTF_CSRF_ENABLED'] = True # Authentication sources -app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal' app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' + app.test_client_class = TestClient test_client = app.test_client() test_client.setApp(app)