public inbox for [email protected]help / color / mirror / Atom feed
[pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication 5+ messages / 2 participants [nested] [flat]
* [pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication @ 2021-04-08 06:50 Khushboo Vashi <[email protected]> 0 siblings, 1 reply; 5+ messages in thread From: Khushboo Vashi @ 2021-04-08 06:50 UTC (permalink / raw) To: pgadmin-hackers Hi, Please find the attached patch for RM 6158: Support Kerberos Authentication - Phase 2. This patch includes the support for logging into PostgreSQL servers with Kerberos authentication. Thanks, Khushboo Attachments: [application/octet-stream] RM_6158.patch (38.8K, 3-RM_6158.patch) download | inline diff: diff --git a/web/config.py b/web/config.py index 3e19a2858..e96d60c65 100644 --- a/web/config.py +++ b/web/config.py @@ -634,6 +634,9 @@ KRB_KTNAME = '<KRB5_KEYTAB_FILE>' KRB_AUTO_CREATE_USER = True +KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache') + + ########################################################################## # Local config settings ########################################################################## diff --git a/web/migrations/versions/cce2006fa107_.py b/web/migrations/versions/cce2006fa107_.py new file mode 100644 index 000000000..0cd3d7da1 --- /dev/null +++ b/web/migrations/versions/cce2006fa107_.py @@ -0,0 +1,28 @@ + +"""empty message + +Revision ID: cce2006fa107 +Revises: a39bd015b644 +Create Date: 2021-03-15 00:02:40.100252 + +""" +from alembic import op +import sqlalchemy as sa +from pgadmin.model import db + +# revision identifiers, used by Alembic. +revision = 'cce2006fa107' +down_revision = 'a39bd015b644' +branch_labels = None +depends_on = None + + +def upgrade(): + db.engine.execute( + 'ALTER TABLE server ADD COLUMN kerberos_conn INTEGER DEFAULT 0' + ) + + +def downgrade(): + # pgAdmin only upgrades, downgrade not implemented. + pass diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index bc0868ddf..2b60979a8 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -14,10 +14,12 @@ import pickle 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 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 import session @@ -35,7 +37,9 @@ class AuthenticateModule(PgAdminModule): def get_exposed_url_endpoints(self): return ['authenticate.login', 'authenticate.kerberos_login', - 'authenticate.kerberos_logout'] + 'authenticate.kerberos_logout', + 'authenticate.kerberos_update_ticket', + 'authenticate.kerberos_validate_ticket'] blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='') @@ -56,6 +60,12 @@ def kerberos_login(): @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'), )) @@ -174,11 +184,13 @@ class AuthSourceManager(): # 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 and (hasattr(msg, 'status') and - msg.status == '401 UNAUTHORIZED') or \ - (source.get_source_name() == KERBEROS and - request.method == 'GET'): - break + 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) @@ -225,3 +237,58 @@ def init_app(app): AuthSourceRegistry.load_auth_sources() return auth_sources + + [email protected]("/kerberos/update_ticket", + endpoint="kerberos_update_ticket", methods=["GET"]) [email protected] +@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) + + [email protected]("/kerberos/validate_ticket", + endpoint="kerberos_validate_ticket", methods=["GET"]) [email protected] +@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/kerberos.py b/web/pgadmin/authenticate/kerberos.py index 57aa1e0f0..2f8fd0d6e 100644 --- a/web/pgadmin/authenticate/kerberos.py +++ b/web/pgadmin/authenticate/kerberos.py @@ -10,7 +10,7 @@ """A blueprint module implementing the Spnego/Kerberos authentication.""" import base64 -from os import environ +from os import environ, path from werkzeug.datastructures import Headers from flask_babelex import gettext @@ -128,19 +128,37 @@ class KerberosAuthentication(BaseAuthentication): if out_token and not context.complete: return False, out_token if context.complete: + deleg_creds = context.delegated_creds + if not hasattr(deleg_creds, 'name'): + error_msg = gettext('Delegated credentials not supplied.') + current_app.logger.error(error_msg) + return False, Exception(error_msg) + try: + cache_file_path = path.join( + config.KERBEROS_CCACHE_DIR, 'pgadmin_cache_{0}'.format( + deleg_creds.name) + ) + CCACHE = 'FILE:{0}'.format(cache_file_path) + store = {'ccache': CCACHE} + deleg_creds.store(store, overwrite=True, set_default=True) + session['KRB5CCNAME'] = CCACHE + except Exception as e: + current_app.logger.exception(e) + return False, e + return True, context else: return False, None def negotiate_end(self, context): - # Free gss_cred_id_t + # Free Delegated Credentials 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.""" + """Add the kerberos user to the internal SQLite database.""" username = str(username) if config.KRB_AUTO_CREATE_USER: user = User.query.filter_by( diff --git a/web/pgadmin/authenticate/static/js/kerberos.js b/web/pgadmin/authenticate/static/js/kerberos.js new file mode 100644 index 000000000..dffe1d4dc --- /dev/null +++ b/web/pgadmin/authenticate/static/js/kerberos.js @@ -0,0 +1,55 @@ +import url_for from 'sources/url_for'; + +function fetch_ticket() { + // Fetch the Kerberos Updated ticket through SPNEGO + return fetch(url_for('authenticate.kerberos_update_ticket') + ) + .then(function(response){ + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response); + } else { + return Promise.reject(new Error(response.statusText)); + } + }); +} + +function fetch_ticket_lifetime () { + // Fetch the Kerberos ticket lifetime left + + return fetch(url_for('authenticate.kerberos_validate_ticket') + ) + .then( + function(response){ + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + return Promise.reject(new Error(response.statusText)); + } + } + ) + .then(function(response){ + let ticket_lifetime = response.data.ticket_lifetime; + if (ticket_lifetime > 0) { + return Promise.resolve(ticket_lifetime); + } else { + return Promise.reject(); + } + }); + +} + +function validate_kerberos_ticket() { + // Ping pgAdmin server every 10 seconds + // to fetch the Kerberos ticket lifetime left + return setInterval(function() { + let newPromise = fetch_ticket_lifetime(); + newPromise.then( + function() { + return; + }, + fetch_ticket + ); + }, 10000); +} + +export {fetch_ticket, validate_kerberos_ticket, fetch_ticket_lifetime}; diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index bc0bd4611..a7e5288e5 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -253,7 +253,8 @@ class ServerModule(sg.ServerGroupPluginModule): errmsg=errmsg, user_id=server.user_id, user_name=server.username, - shared=server.shared + shared=server.shared, + is_kerberos_conn=bool(server.kerberos_conn), ) @property @@ -546,7 +547,8 @@ class ServerNode(PGChildNodeView): if server.tunnel_password is not None else False, errmsg=errmsg, user_name=server.username, - shared=server.shared + shared=server.shared, + is_kerberos_conn=bool(server.kerberos_conn) ) ) @@ -613,7 +615,8 @@ class ServerNode(PGChildNodeView): if server.tunnel_password is not None else False, errmsg=errmsg, shared=server.shared, - user_name=server.username + user_name=server.username, + is_kerberos_conn=bool(server.kerberos_conn) ), ) @@ -719,7 +722,8 @@ class ServerNode(PGChildNodeView): 'tunnel_username': 'tunnel_username', 'tunnel_authentication': 'tunnel_authentication', 'tunnel_identity_file': 'tunnel_identity_file', - 'shared': 'shared' + 'shared': 'shared', + 'kerberos_conn': 'kerberos_conn', } disp_lbl = { @@ -983,7 +987,8 @@ class ServerNode(PGChildNodeView): 'tunnel_username': tunnel_username, 'tunnel_identity_file': server.tunnel_identity_file if server.tunnel_identity_file else None, - 'tunnel_authentication': tunnel_authentication + 'tunnel_authentication': tunnel_authentication, + 'kerberos_conn': bool(server.kerberos_conn), } return ajax_response(response) @@ -1070,7 +1075,8 @@ class ServerNode(PGChildNodeView): tunnel_authentication=data.get('tunnel_authentication', 0), tunnel_identity_file=data.get('tunnel_identity_file', None), shared=data.get('shared', None), - passfile=data.get('passfile', None) + passfile=data.get('passfile', None), + kerberos_conn=1 if data.get('kerberos_conn', False) else 0, ) db.session.add(server) db.session.commit() @@ -1152,7 +1158,8 @@ class ServerNode(PGChildNodeView): else 'pg', version=manager.version if manager and manager.version - else None + else None, + is_kerberos_conn=bool(server.kerberos_conn), ) ) @@ -1346,7 +1353,7 @@ class ServerNode(PGChildNodeView): except Exception as e: current_app.logger.exception(e) return internal_server_error(errormsg=str(e)) - if 'password' not in data: + if 'password' not in data and server.kerberos_conn is False: conn_passwd = getattr(conn, 'password', None) if conn_passwd is None and not server.save_password and \ server.passfile is None and server.service is None: @@ -1355,7 +1362,7 @@ class ServerNode(PGChildNodeView): passfile = server.passfile else: password = conn_passwd or server.password - else: + elif server.kerberos_conn is False: password = data['password'] if 'password' in data else None save_password = data['save_password']\ if 'save_password' in data else False @@ -1398,6 +1405,9 @@ class ServerNode(PGChildNodeView): "Could not connect to server(#{0}) - '{1}'.\nError: {2}" .format(server.id, server.name, errmsg) ) + if errmsg.find('Ticket expired') != -1: + return internal_server_error(errmsg) + return self.get_response_for_password(server, 401, True, True, errmsg) else: @@ -1465,6 +1475,7 @@ class ServerNode(PGChildNodeView): 'is_password_saved': bool(server.save_password), 'is_tunnel_password_saved': True if server.tunnel_password is not None else False, + 'is_kerberos_conn': bool(server.kerberos_conn), } ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py index 60af1de42..4b1d7308d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py @@ -490,6 +490,7 @@ class DatabaseView(PGChildNodeView): did, errmsg ) ) + return internal_server_error(errmsg) else: current_app.logger.info( diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index c53f04429..01ab89c50 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -10,9 +10,10 @@ define('pgadmin.node.database', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'sources/utils', 'sources/pgadmin', 'pgadmin.browser.utils', - 'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.collection', + 'pgadmin.alertifyjs', 'pgadmin.backform', + 'pgadmin.authenticate.kerberos', 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege', 'pgadmin.browser.server.variable', -], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform) { +], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform, Kerberos) { if (!pgBrowser.Nodes['coll-database']) { pgBrowser.Nodes['coll-database'] = @@ -556,24 +557,39 @@ define('pgadmin.node.database', [ onFailure = function( xhr, status, error, _model, _data, _tree, _item, _status ) { - if (!_status) { - tree.setInode(_item); - tree.addIcon(_item, {icon: 'icon-database-not-connected'}); - } - - Alertify.pgNotifier('error', xhr, error, function(msg) { - setTimeout(function() { - if (msg == 'CRYPTKEY_SET') { + if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) { + tree.addIcon(_item, {icon: 'icon-server-connecting'}); + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { connect_to_database(_model, _data, _tree, _item, _wasConnected); - } else { - Alertify.dlgServerPass( - gettext('Connect to database'), - msg, _model, _data, _tree, _item, _status, - onSuccess, onFailure, onCancel - ).resizeTo(); + }, + function(error) { + tree.setInode(_item); + tree.addIcon(_item, {icon: 'icon-database-not-connected'}); + Alertify.pgNotifier(error, xhr, gettext('Connect to database.')); } - }, 100); - }); + ); + } else { + if (!_status) { + tree.setInode(_item); + tree.addIcon(_item, {icon: 'icon-database-not-connected'}); + } + + Alertify.pgNotifier('error', xhr, error, function(msg) { + setTimeout(function() { + if (msg == 'CRYPTKEY_SET') { + connect_to_database(_model, _data, _tree, _item, _wasConnected); + } else { + Alertify.dlgServerPass( + gettext('Connect to database'), + msg, _model, _data, _tree, _item, _status, + onSuccess, onFailure, onCancel + ).resizeTo(); + } + }, 100); + }); + } }, onSuccess = function( res, model, _data, _tree, _item, _connected @@ -640,6 +656,7 @@ define('pgadmin.node.database', [ if (xhr.status === 410) { error = gettext('Error: Object not found - %s.', error); } + return onFailure( xhr, status, error, obj, data, tree, item, wasConnected ); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index ab95d6d89..622cd776a 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -13,11 +13,12 @@ define('pgadmin.node.server', [ 'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user', 'pgadmin.alertifyjs', 'pgadmin.backform', 'sources/browser/server_groups/servers/model_validation', + 'pgadmin.authenticate.kerberos', 'pgadmin.browser.server.privilege', ], function( gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, supported_servers, current_user, Alertify, Backform, - modelValidation + modelValidation, Kerberos ) { if (!pgBrowser.Nodes['server']) { @@ -922,6 +923,11 @@ define('pgadmin.node.server', [ return false; }, + },{ + id: 'kerberos_conn', label: gettext('Kerberos Connection??'), type: 'switch', + group: gettext('Connection'), 'options': { + 'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini', + }, },{ id: 'role', label: gettext('Role'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], readonly: 'isConnected', @@ -1279,19 +1285,32 @@ define('pgadmin.node.server', [ } } - - Alertify.pgNotifier('error', xhr, error, function(msg) { - setTimeout(function() { - if (msg == 'CRYPTKEY_SET') { + if (_data.is_kerberos_conn === true || (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1)) { + tree.addIcon(_item, {icon: 'icon-server-connecting'}); + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { connect_to_server(_node, _data, _tree, _item, _wasConnected); - } else { - Alertify.dlgServerPass( - gettext('Connect to Server'), - msg, _node, _data, _tree, _item, _wasConnected - ).resizeTo(); + }, + function() { + tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + Alertify.pgNotifier('Connection error', xhr, gettext('Connect to server.')); } - }, 100); - }); + ); + } else { + Alertify.pgNotifier('error', xhr, error, function(msg) { + setTimeout(function() { + if (msg == 'CRYPTKEY_SET') { + connect_to_server(_node, _data, _tree, _item, _wasConnected); + } else { + Alertify.dlgServerPass( + gettext('Connect to Server'), + msg, _node, _data, _tree, _item, _wasConnected + ).resizeTo(); + } + }, 100); + }); + } }, onSuccess = function(res, node, _data, _tree, _item, _wasConnected) { if (res && res.data) { diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 4ffb5ee5a..bf44aa6f4 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -12,19 +12,22 @@ define('pgadmin.browser', [ 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', 'sources/check_node_visibility', './toolbar', 'pgadmin.help', - 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.browser.utils', - 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', + 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.authenticate.kerberos', + 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', + 'jquery.acitree', 'pgadmin.browser.preferences', 'pgadmin.browser.messages', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', 'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', - 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment', + 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', + 'jquery.acifragment', ], function( tree, gettext, url_for, require, $, _, Bootstrap, pgAdmin, Alertify, codemirror, - checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow + checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow, + Kerberos ) { window.jQuery = window.$ = $; // Some scripts do export their object in the window only. @@ -38,6 +41,8 @@ define('pgadmin.browser', [ csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); + Kerberos.validate_kerberos_ticket(); + var panelEvents = {}; panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() { if (this.isVisible()) { diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py index f31e983ff..6b61dc1d0 100644 --- a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py +++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py @@ -12,6 +12,7 @@ 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 werkzeug.datastructures import Headers class KerberosLoginMockTestCase(BaseTestGenerator): @@ -30,6 +31,11 @@ class KerberosLoginMockTestCase(BaseTestGenerator): auth_source=['kerberos'], auto_create_user=True, flag=2 + )), + ('Spnego/Kerberos Update Ticket', dict( + auth_source=['kerberos'], + auto_create_user=True, + flag=3 )) ] @@ -54,8 +60,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator): self.skipTest( "Can not run Kerberos Authentication in the Desktop mode." ) - self.test_authorized() + elif self.flag == 3: + if app_config.SERVER_MODE is False: + self.skipTest( + "Can not run Kerberos Authentication in the Desktop mode." + ) + self.test_update_ticket() def test_unauthorized(self): """ @@ -73,13 +84,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator): passed on to the routed method. """ - class delCrads: - def __init__(self): - self.initiator_name = '[email protected]' - del_crads = delCrads() - - AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock( - return_value=[True, del_crads]) + del_crads = self.mock_negotiate_start() res = self.tester.login(None, None, True, @@ -89,6 +94,33 @@ class KerberosLoginMockTestCase(BaseTestGenerator): respdata = 'Gravatar image for %s' % del_crads.initiator_name self.assertTrue(respdata in res.data.decode('utf8')) + def mock_negotiate_start(self): + class delCrads: + def __init__(self): + self.initiator_name = '[email protected]' + + del_crads = delCrads() + + 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') + self.assertEqual(response.status_code, 401) + self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate') + + # When we send the Kerberos Ticket, it should return success + del_crads = self.mock_negotiate_start() + + krb_token = Headers({}) + krb_token['Authorization'] = 'Negotiate CTOKEN' + + response = self.tester.get('/authenticate/kerberos/update_ticket', + headers=krb_token) + self.assertEqual(response.status_code, 200) + def tearDown(self): self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py index ef6cfc3f2..25e0a2a9e 100644 --- a/web/pgadmin/misc/bgprocess/processes.py +++ b/web/pgadmin/misc/bgprocess/processes.py @@ -24,10 +24,11 @@ import logging from pgadmin.utils import u_encode, file_quote, fs_encoding, \ get_complete_file_path, get_storage_directory, IS_WIN from pgadmin.browser.server_groups.servers.utils import does_server_exists +from pgadmin.utils.constants import KERBEROS import pytz from dateutil import parser -from flask import current_app +from flask import current_app, session from flask_babelex import gettext as _ from flask_security import current_user @@ -278,13 +279,16 @@ class BatchProcess(object): env['PROCID'] = self.id env['OUTDIR'] = self.log_dir env['PGA_BGP_FOREGROUND'] = "1" + if config.SERVER_MODE and session and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS: + env['KRB5CCNAME'] = session['KRB5CCNAME'] if self.env: env.update(self.env) if cb is not None: cb(env) - if os.name == 'nt': DETACHED_PROCESS = 0x00000008 from subprocess import CREATE_NEW_PROCESS_GROUP diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index d1f498181..dfacf6322 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy # ########################################################################## -SCHEMA_VERSION = 27 +SCHEMA_VERSION = 28 ########################################################################## # @@ -184,6 +184,7 @@ class Server(db.Model): tunnel_identity_file = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(db.String(64), nullable=True) shared = db.Column(db.Boolean(), nullable=False) + kerberos_conn = db.Column(db.Boolean(), nullable=False) @property def serialize(self): diff --git a/web/pgadmin/setup/data_directory.py b/web/pgadmin/setup/data_directory.py index 2335b0790..7a3654b77 100644 --- a/web/pgadmin/setup/data_directory.py +++ b/web/pgadmin/setup/data_directory.py @@ -104,3 +104,19 @@ def create_app_data_directory(config): getpass.getuser(), config.APP_VERSION)) exit(1) + + # Create Kerberos Credential Cache directory (if not present). + try: + _create_directory_if_not_exists(config.KERBEROS_CCACHE_DIR) + except PermissionError as e: + print(FAILED_CREATE_DIR.format(config.KERBEROS_CCACHE_DIR, e)) + print( + "HINT : Create the directory {}, ensure it is writable by\n" + " '{}', and try again, or, create a config_local.py file\n" + " and override the KERBEROS_CCACHE_DIR setting per\n" + " https://www.pgadmin.org/docs/pgadmin4/{}/config_py.html". + format( + config.KERBEROS_CCACHE_DIR, + getpass.getuser(), + config.APP_VERSION)) + exit(1) diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js index 4f89e5bb7..a1317467c 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js @@ -13,6 +13,7 @@ import gettext from '../../../../static/js/gettext'; import url_for from '../../../../static/js/url_for'; import _ from 'underscore'; import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper'; +import {fetch_ticket_lifetime} from '../../../../authenticate/static/js/kerberos'; export class BackupDialogWrapper extends DialogWrapper { constructor(dialogContainerSelector, dialogTitle, typeOfDialog, @@ -165,10 +166,29 @@ export class BackupDialogWrapper extends DialogWrapper { ); this.setExtraParameters(selectedTreeNode, treeInfo); + let backupDate = this.view.model.toJSON(); + + if(backupDate.type == 'globals') { + let newPromise = fetch_ticket_lifetime(); + newPromise.then( + function(lifetime) { + if (lifetime < 1800 && lifetime > 0) { + dialog.alertify.warning( + 'You have '+ (Math.round(parseInt(lifetime)/60)).toString() +' minutes left on your ticket - if the dump takes longer than that, it may fail."' + ); + } + }, + function() { + dialog.alertify.warning( + gettext('Please renew your kerberos ticket, it has been expired.') + ); + } + ); + } axios.post( baseUrl, - this.view.model.toJSON() + backupDate ).then(function (res) { if (res.data.success) { dialog.alertify.success(gettext('Backup job created.'), 5); diff --git a/web/pgadmin/tools/debugger/static/js/debugger.js b/web/pgadmin/tools/debugger/static/js/debugger.js index f31a0fc00..460a200bb 100644 --- a/web/pgadmin/tools/debugger/static/js/debugger.js +++ b/web/pgadmin/tools/debugger/static/js/debugger.js @@ -13,11 +13,11 @@ define([ 'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform', 'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils', 'tools/datagrid/static/js/show_query_tool', 'sources/utils', - 'wcdocker', 'pgadmin.browser.frame', + 'pgadmin.authenticate.kerberos', 'wcdocker', 'pgadmin.browser.frame', ], function( gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, CodeMirror, Backform, get_function_arguments, debuggerUtils, showQueryTool, - pgadminUtils, + pgadminUtils, Kerberos ) { var pgTools = pgAdmin.Tools = pgAdmin.Tools || {}, wcDocker = window.wcDocker; @@ -472,8 +472,20 @@ define([ .fail(function(xhr) { try { var err = JSON.parse(xhr.responseText); - if (err.success == 0) { - Alertify.alert(gettext('Debugger Error'), err.errormsg); + if (err.errormsg.search('Ticket expired') !== -1) { + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { + self.start_global_debugger(); + }, + function(error) { + Alertify.alert(gettext('Debugger Error'), error); + } + ); + } else { + if (err.success == 0) { + Alertify.alert(gettext('Debugger Error'), err.errormsg); + } } } catch (e) { console.warn(e.stack || e); diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 5da66369d..596c9818f 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -51,6 +51,7 @@ define('tools.querytool', [ 'sources/window', 'sources/is_native', 'sources/sqleditor/macro', + 'pgadmin.authenticate.kerberos', 'sources/../bundle/slickgrid', 'pgadmin.file_manager', 'slick.pgadmin.formatters', @@ -65,7 +66,7 @@ define('tools.querytool', [ GeometryViewer, historyColl, queryHist, querySources, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc, - pgWindow, isNative, MacroHandler) { + pgWindow, isNative, MacroHandler, Kerberos) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) return pgAdmin.SqlEditor; @@ -2431,9 +2432,23 @@ define('tools.querytool', [ pgBrowser.report_error(gettext('Error fetching rows - %s.', xhr.statusText), xhr.responseJSON.errormsg, undefined, self.close.bind(self)); } } else { - pgBrowser.Events.trigger( - 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error - ); + if (xhr.responseText.search('Ticket expired') !== -1) { + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { + self.initTransaction(); + }, + function(error) { + pgBrowser.Events.trigger( + 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error + ); + } + ); + } else { + pgBrowser.Events.trigger( + 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error + ); + } } }); }, diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index de9547322..43f6e6de0 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -18,11 +18,13 @@ import select import datetime from collections import deque import psycopg2 -from flask import g, current_app +import threading +from flask import g, current_app, session from flask_babelex import gettext from flask_security import current_user from pgadmin.utils.crypto import decrypt, encrypt from psycopg2.extensions import encodings +from os import environ import config from pgadmin.model import User @@ -38,6 +40,9 @@ from .encoding import get_encoding, configure_driver_encodings from pgadmin.utils import csv from pgadmin.utils.master_password import get_crypt_key from io import StringIO +from pgadmin.utils.constants import KERBEROS + +lock = threading.Lock() _ = gettext @@ -313,6 +318,12 @@ class Connection(BaseConnection): os.environ['PGAPPNAME'] = '{0} - {1}'.format( config.APP_NAME, conn_id) + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS and 'KRB5CCNAME' in session: + lock.acquire() + environ['KRB5CCNAME'] = session['KRB5CCNAME'] + pg_conn = psycopg2.connect( host=manager.local_bind_host if manager.use_ssh_tunnel else manager.host, @@ -340,7 +351,13 @@ class Connection(BaseConnection): if self.async_ == 1: self._wait(pg_conn) + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS: + environ['KRB5CCNAME'] = '' + except psycopg2.Error as e: + environ['KRB5CCNAME'] = '' manager.stop_ssh_tunnel() if e.pgerror: msg = e.pgerror @@ -358,6 +375,11 @@ class Connection(BaseConnection): ) ) return False, msg + finally: + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS and lock.locked(): + lock.release() # Overwrite connection notice attr to support # more than 50 notices at a time @@ -1435,7 +1457,6 @@ Failed to reset the connection to the server due to following error: Args: conn: connection object """ - while True: state = conn.poll() if state == psycopg2.extensions.POLL_OK: diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 96d5b27f6..00daa12ec 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -174,6 +174,7 @@ var webpackShimConfig = { 'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'), 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'), + 'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'), 'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'), 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'), 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'), ^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: [pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication @ 2021-04-14 08:35 Khushboo Vashi <[email protected]> parent: Khushboo Vashi <[email protected]> 0 siblings, 1 reply; 5+ messages in thread From: Khushboo Vashi @ 2021-04-14 08:35 UTC (permalink / raw) To: pgadmin-hackers Hi, Please find the attached patch with some minor improvements. Thanks, Khushboo On Wed, Apr 7, 2021 at 11:50 PM Khushboo Vashi < [email protected]> wrote: > Hi, > > Please find the attached patch for RM 6158: Support Kerberos > Authentication - Phase 2. > This patch includes the support for logging into PostgreSQL servers with > Kerberos authentication. > > Thanks, > Khushboo > > Attachments: [text/x-patch] RM_6158_v1.patch (39.9K, 3-RM_6158_v1.patch) download | inline diff: diff --git a/web/config.py b/web/config.py index 3e19a2858..e96d60c65 100644 --- a/web/config.py +++ b/web/config.py @@ -634,6 +634,9 @@ KRB_KTNAME = '<KRB5_KEYTAB_FILE>' KRB_AUTO_CREATE_USER = True +KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache') + + ########################################################################## # Local config settings ########################################################################## diff --git a/web/migrations/versions/cce2006fa107_.py b/web/migrations/versions/cce2006fa107_.py new file mode 100644 index 000000000..0cd3d7da1 --- /dev/null +++ b/web/migrations/versions/cce2006fa107_.py @@ -0,0 +1,28 @@ + +"""empty message + +Revision ID: cce2006fa107 +Revises: a39bd015b644 +Create Date: 2021-03-15 00:02:40.100252 + +""" +from alembic import op +import sqlalchemy as sa +from pgadmin.model import db + +# revision identifiers, used by Alembic. +revision = 'cce2006fa107' +down_revision = 'a39bd015b644' +branch_labels = None +depends_on = None + + +def upgrade(): + db.engine.execute( + 'ALTER TABLE server ADD COLUMN kerberos_conn INTEGER DEFAULT 0' + ) + + +def downgrade(): + # pgAdmin only upgrades, downgrade not implemented. + pass diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index 9166c2ffd..3bc64483e 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -13,10 +13,13 @@ import flask import pickle from flask import current_app, flash, Response, request, url_for,\ render_template -from flask_security import current_user +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 import session @@ -34,7 +37,9 @@ class AuthenticateModule(PgAdminModule): def get_exposed_url_endpoints(self): return ['authenticate.login', 'authenticate.kerberos_login', - 'authenticate.kerberos_logout'] + 'authenticate.kerberos_logout', + 'authenticate.kerberos_update_ticket', + 'authenticate.kerberos_validate_ticket'] blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='') @@ -55,6 +60,12 @@ def kerberos_login(): @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'), )) @@ -173,11 +184,13 @@ class AuthSourceManager(): # 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 and (hasattr(msg, 'status') and - msg.status == '401 UNAUTHORIZED') or \ - (source.get_source_name() == KERBEROS and - request.method == 'GET'): - break + 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) @@ -224,3 +237,58 @@ def init_app(app): AuthSourceRegistry.load_auth_sources() return auth_sources + + [email protected]("/kerberos/update_ticket", + endpoint="kerberos_update_ticket", methods=["GET"]) [email protected] +@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) + + [email protected]("/kerberos/validate_ticket", + endpoint="kerberos_validate_ticket", methods=["GET"]) [email protected] +@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/kerberos.py b/web/pgadmin/authenticate/kerberos.py index 57aa1e0f0..2f8fd0d6e 100644 --- a/web/pgadmin/authenticate/kerberos.py +++ b/web/pgadmin/authenticate/kerberos.py @@ -10,7 +10,7 @@ """A blueprint module implementing the Spnego/Kerberos authentication.""" import base64 -from os import environ +from os import environ, path from werkzeug.datastructures import Headers from flask_babelex import gettext @@ -128,19 +128,37 @@ class KerberosAuthentication(BaseAuthentication): if out_token and not context.complete: return False, out_token if context.complete: + deleg_creds = context.delegated_creds + if not hasattr(deleg_creds, 'name'): + error_msg = gettext('Delegated credentials not supplied.') + current_app.logger.error(error_msg) + return False, Exception(error_msg) + try: + cache_file_path = path.join( + config.KERBEROS_CCACHE_DIR, 'pgadmin_cache_{0}'.format( + deleg_creds.name) + ) + CCACHE = 'FILE:{0}'.format(cache_file_path) + store = {'ccache': CCACHE} + deleg_creds.store(store, overwrite=True, set_default=True) + session['KRB5CCNAME'] = CCACHE + except Exception as e: + current_app.logger.exception(e) + return False, e + return True, context else: return False, None def negotiate_end(self, context): - # Free gss_cred_id_t + # Free Delegated Credentials 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.""" + """Add the kerberos user to the internal SQLite database.""" username = str(username) if config.KRB_AUTO_CREATE_USER: user = User.query.filter_by( diff --git a/web/pgadmin/authenticate/static/js/kerberos.js b/web/pgadmin/authenticate/static/js/kerberos.js new file mode 100644 index 000000000..dffe1d4dc --- /dev/null +++ b/web/pgadmin/authenticate/static/js/kerberos.js @@ -0,0 +1,55 @@ +import url_for from 'sources/url_for'; + +function fetch_ticket() { + // Fetch the Kerberos Updated ticket through SPNEGO + return fetch(url_for('authenticate.kerberos_update_ticket') + ) + .then(function(response){ + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response); + } else { + return Promise.reject(new Error(response.statusText)); + } + }); +} + +function fetch_ticket_lifetime () { + // Fetch the Kerberos ticket lifetime left + + return fetch(url_for('authenticate.kerberos_validate_ticket') + ) + .then( + function(response){ + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + return Promise.reject(new Error(response.statusText)); + } + } + ) + .then(function(response){ + let ticket_lifetime = response.data.ticket_lifetime; + if (ticket_lifetime > 0) { + return Promise.resolve(ticket_lifetime); + } else { + return Promise.reject(); + } + }); + +} + +function validate_kerberos_ticket() { + // Ping pgAdmin server every 10 seconds + // to fetch the Kerberos ticket lifetime left + return setInterval(function() { + let newPromise = fetch_ticket_lifetime(); + newPromise.then( + function() { + return; + }, + fetch_ticket + ); + }, 10000); +} + +export {fetch_ticket, validate_kerberos_ticket, fetch_ticket_lifetime}; diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index bc0bd4611..a7e5288e5 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -253,7 +253,8 @@ class ServerModule(sg.ServerGroupPluginModule): errmsg=errmsg, user_id=server.user_id, user_name=server.username, - shared=server.shared + shared=server.shared, + is_kerberos_conn=bool(server.kerberos_conn), ) @property @@ -546,7 +547,8 @@ class ServerNode(PGChildNodeView): if server.tunnel_password is not None else False, errmsg=errmsg, user_name=server.username, - shared=server.shared + shared=server.shared, + is_kerberos_conn=bool(server.kerberos_conn) ) ) @@ -613,7 +615,8 @@ class ServerNode(PGChildNodeView): if server.tunnel_password is not None else False, errmsg=errmsg, shared=server.shared, - user_name=server.username + user_name=server.username, + is_kerberos_conn=bool(server.kerberos_conn) ), ) @@ -719,7 +722,8 @@ class ServerNode(PGChildNodeView): 'tunnel_username': 'tunnel_username', 'tunnel_authentication': 'tunnel_authentication', 'tunnel_identity_file': 'tunnel_identity_file', - 'shared': 'shared' + 'shared': 'shared', + 'kerberos_conn': 'kerberos_conn', } disp_lbl = { @@ -983,7 +987,8 @@ class ServerNode(PGChildNodeView): 'tunnel_username': tunnel_username, 'tunnel_identity_file': server.tunnel_identity_file if server.tunnel_identity_file else None, - 'tunnel_authentication': tunnel_authentication + 'tunnel_authentication': tunnel_authentication, + 'kerberos_conn': bool(server.kerberos_conn), } return ajax_response(response) @@ -1070,7 +1075,8 @@ class ServerNode(PGChildNodeView): tunnel_authentication=data.get('tunnel_authentication', 0), tunnel_identity_file=data.get('tunnel_identity_file', None), shared=data.get('shared', None), - passfile=data.get('passfile', None) + passfile=data.get('passfile', None), + kerberos_conn=1 if data.get('kerberos_conn', False) else 0, ) db.session.add(server) db.session.commit() @@ -1152,7 +1158,8 @@ class ServerNode(PGChildNodeView): else 'pg', version=manager.version if manager and manager.version - else None + else None, + is_kerberos_conn=bool(server.kerberos_conn), ) ) @@ -1346,7 +1353,7 @@ class ServerNode(PGChildNodeView): except Exception as e: current_app.logger.exception(e) return internal_server_error(errormsg=str(e)) - if 'password' not in data: + if 'password' not in data and server.kerberos_conn is False: conn_passwd = getattr(conn, 'password', None) if conn_passwd is None and not server.save_password and \ server.passfile is None and server.service is None: @@ -1355,7 +1362,7 @@ class ServerNode(PGChildNodeView): passfile = server.passfile else: password = conn_passwd or server.password - else: + elif server.kerberos_conn is False: password = data['password'] if 'password' in data else None save_password = data['save_password']\ if 'save_password' in data else False @@ -1398,6 +1405,9 @@ class ServerNode(PGChildNodeView): "Could not connect to server(#{0}) - '{1}'.\nError: {2}" .format(server.id, server.name, errmsg) ) + if errmsg.find('Ticket expired') != -1: + return internal_server_error(errmsg) + return self.get_response_for_password(server, 401, True, True, errmsg) else: @@ -1465,6 +1475,7 @@ class ServerNode(PGChildNodeView): 'is_password_saved': bool(server.save_password), 'is_tunnel_password_saved': True if server.tunnel_password is not None else False, + 'is_kerberos_conn': bool(server.kerberos_conn), } ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py index 60af1de42..4b1d7308d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py @@ -490,6 +490,7 @@ class DatabaseView(PGChildNodeView): did, errmsg ) ) + return internal_server_error(errmsg) else: current_app.logger.info( diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index c53f04429..01ab89c50 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -10,9 +10,10 @@ define('pgadmin.node.database', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'sources/utils', 'sources/pgadmin', 'pgadmin.browser.utils', - 'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.collection', + 'pgadmin.alertifyjs', 'pgadmin.backform', + 'pgadmin.authenticate.kerberos', 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege', 'pgadmin.browser.server.variable', -], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform) { +], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform, Kerberos) { if (!pgBrowser.Nodes['coll-database']) { pgBrowser.Nodes['coll-database'] = @@ -556,24 +557,39 @@ define('pgadmin.node.database', [ onFailure = function( xhr, status, error, _model, _data, _tree, _item, _status ) { - if (!_status) { - tree.setInode(_item); - tree.addIcon(_item, {icon: 'icon-database-not-connected'}); - } - - Alertify.pgNotifier('error', xhr, error, function(msg) { - setTimeout(function() { - if (msg == 'CRYPTKEY_SET') { + if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) { + tree.addIcon(_item, {icon: 'icon-server-connecting'}); + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { connect_to_database(_model, _data, _tree, _item, _wasConnected); - } else { - Alertify.dlgServerPass( - gettext('Connect to database'), - msg, _model, _data, _tree, _item, _status, - onSuccess, onFailure, onCancel - ).resizeTo(); + }, + function(error) { + tree.setInode(_item); + tree.addIcon(_item, {icon: 'icon-database-not-connected'}); + Alertify.pgNotifier(error, xhr, gettext('Connect to database.')); } - }, 100); - }); + ); + } else { + if (!_status) { + tree.setInode(_item); + tree.addIcon(_item, {icon: 'icon-database-not-connected'}); + } + + Alertify.pgNotifier('error', xhr, error, function(msg) { + setTimeout(function() { + if (msg == 'CRYPTKEY_SET') { + connect_to_database(_model, _data, _tree, _item, _wasConnected); + } else { + Alertify.dlgServerPass( + gettext('Connect to database'), + msg, _model, _data, _tree, _item, _status, + onSuccess, onFailure, onCancel + ).resizeTo(); + } + }, 100); + }); + } }, onSuccess = function( res, model, _data, _tree, _item, _connected @@ -640,6 +656,7 @@ define('pgadmin.node.database', [ if (xhr.status === 410) { error = gettext('Error: Object not found - %s.', error); } + return onFailure( xhr, status, error, obj, data, tree, item, wasConnected ); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index ab95d6d89..deee434bd 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -13,11 +13,12 @@ define('pgadmin.node.server', [ 'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user', 'pgadmin.alertifyjs', 'pgadmin.backform', 'sources/browser/server_groups/servers/model_validation', + 'pgadmin.authenticate.kerberos', 'pgadmin.browser.server.privilege', ], function( gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, supported_servers, current_user, Alertify, Backform, - modelValidation + modelValidation, Kerberos ) { if (!pgBrowser.Nodes['server']) { @@ -904,20 +905,32 @@ define('pgadmin.node.server', [ } }, }), + },{ + id: 'kerberos_conn', label: gettext('Kerberos authentication?'), type: 'switch', + group: gettext('Connection'), 'options': { + 'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini', + }, },{ id: 'password', label: gettext('Password'), type: 'password', maxlength: null, - group: gettext('Connection'), control: 'input', mode: ['create'], deps: ['connect_now'], + group: gettext('Connection'), control: 'input', mode: ['create'], + deps: ['connect_now', 'kerberos_conn'], visible: function(model) { return model.get('connect_now') && model.isNew(); }, + disabled: function(model) { + if (model.get('kerberos_conn')) + return true; + + return false; + }, },{ id: 'save_password', controlLabel: gettext('Save password?'), type: 'checkbox', group: gettext('Connection'), mode: ['create'], - deps: ['connect_now'], visible: function(model) { + deps: ['connect_now', 'kerberos_conn'], visible: function(model) { return model.get('connect_now') && model.isNew(); }, - disabled: function() { - if (!current_user.allow_save_password) + disabled: function(model) { + if (!current_user.allow_save_password || model.get('kerberos_conn')) return true; return false; @@ -1279,19 +1292,32 @@ define('pgadmin.node.server', [ } } - - Alertify.pgNotifier('error', xhr, error, function(msg) { - setTimeout(function() { - if (msg == 'CRYPTKEY_SET') { + if (_data.is_kerberos_conn === true || (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1)) { + tree.addIcon(_item, {icon: 'icon-server-connecting'}); + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { connect_to_server(_node, _data, _tree, _item, _wasConnected); - } else { - Alertify.dlgServerPass( - gettext('Connect to Server'), - msg, _node, _data, _tree, _item, _wasConnected - ).resizeTo(); + }, + function() { + tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + Alertify.pgNotifier('Connection error', xhr, gettext('Connect to server.')); } - }, 100); - }); + ); + } else { + Alertify.pgNotifier('error', xhr, error, function(msg) { + setTimeout(function() { + if (msg == 'CRYPTKEY_SET') { + connect_to_server(_node, _data, _tree, _item, _wasConnected); + } else { + Alertify.dlgServerPass( + gettext('Connect to Server'), + msg, _node, _data, _tree, _item, _wasConnected + ).resizeTo(); + } + }, 100); + }); + } }, onSuccess = function(res, node, _data, _tree, _item, _wasConnected) { if (res && res.data) { diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 4ffb5ee5a..bf44aa6f4 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -12,19 +12,22 @@ define('pgadmin.browser', [ 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', 'sources/check_node_visibility', './toolbar', 'pgadmin.help', - 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.browser.utils', - 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', + 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.authenticate.kerberos', + 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', + 'jquery.acitree', 'pgadmin.browser.preferences', 'pgadmin.browser.messages', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', 'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', - 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment', + 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', + 'jquery.acifragment', ], function( tree, gettext, url_for, require, $, _, Bootstrap, pgAdmin, Alertify, codemirror, - checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow + checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow, + Kerberos ) { window.jQuery = window.$ = $; // Some scripts do export their object in the window only. @@ -38,6 +41,8 @@ define('pgadmin.browser', [ csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); + Kerberos.validate_kerberos_ticket(); + var panelEvents = {}; panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() { if (this.isVisible()) { diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py index f31e983ff..6b61dc1d0 100644 --- a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py +++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py @@ -12,6 +12,7 @@ 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 werkzeug.datastructures import Headers class KerberosLoginMockTestCase(BaseTestGenerator): @@ -30,6 +31,11 @@ class KerberosLoginMockTestCase(BaseTestGenerator): auth_source=['kerberos'], auto_create_user=True, flag=2 + )), + ('Spnego/Kerberos Update Ticket', dict( + auth_source=['kerberos'], + auto_create_user=True, + flag=3 )) ] @@ -54,8 +60,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator): self.skipTest( "Can not run Kerberos Authentication in the Desktop mode." ) - self.test_authorized() + elif self.flag == 3: + if app_config.SERVER_MODE is False: + self.skipTest( + "Can not run Kerberos Authentication in the Desktop mode." + ) + self.test_update_ticket() def test_unauthorized(self): """ @@ -73,13 +84,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator): passed on to the routed method. """ - class delCrads: - def __init__(self): - self.initiator_name = '[email protected]' - del_crads = delCrads() - - AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock( - return_value=[True, del_crads]) + del_crads = self.mock_negotiate_start() res = self.tester.login(None, None, True, @@ -89,6 +94,33 @@ class KerberosLoginMockTestCase(BaseTestGenerator): respdata = 'Gravatar image for %s' % del_crads.initiator_name self.assertTrue(respdata in res.data.decode('utf8')) + def mock_negotiate_start(self): + class delCrads: + def __init__(self): + self.initiator_name = '[email protected]' + + del_crads = delCrads() + + 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') + self.assertEqual(response.status_code, 401) + self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate') + + # When we send the Kerberos Ticket, it should return success + del_crads = self.mock_negotiate_start() + + krb_token = Headers({}) + krb_token['Authorization'] = 'Negotiate CTOKEN' + + response = self.tester.get('/authenticate/kerberos/update_ticket', + headers=krb_token) + self.assertEqual(response.status_code, 200) + def tearDown(self): self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py index ef6cfc3f2..25e0a2a9e 100644 --- a/web/pgadmin/misc/bgprocess/processes.py +++ b/web/pgadmin/misc/bgprocess/processes.py @@ -24,10 +24,11 @@ import logging from pgadmin.utils import u_encode, file_quote, fs_encoding, \ get_complete_file_path, get_storage_directory, IS_WIN from pgadmin.browser.server_groups.servers.utils import does_server_exists +from pgadmin.utils.constants import KERBEROS import pytz from dateutil import parser -from flask import current_app +from flask import current_app, session from flask_babelex import gettext as _ from flask_security import current_user @@ -278,13 +279,16 @@ class BatchProcess(object): env['PROCID'] = self.id env['OUTDIR'] = self.log_dir env['PGA_BGP_FOREGROUND'] = "1" + if config.SERVER_MODE and session and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS: + env['KRB5CCNAME'] = session['KRB5CCNAME'] if self.env: env.update(self.env) if cb is not None: cb(env) - if os.name == 'nt': DETACHED_PROCESS = 0x00000008 from subprocess import CREATE_NEW_PROCESS_GROUP diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index d1f498181..dfacf6322 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy # ########################################################################## -SCHEMA_VERSION = 27 +SCHEMA_VERSION = 28 ########################################################################## # @@ -184,6 +184,7 @@ class Server(db.Model): tunnel_identity_file = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(db.String(64), nullable=True) shared = db.Column(db.Boolean(), nullable=False) + kerberos_conn = db.Column(db.Boolean(), nullable=False) @property def serialize(self): diff --git a/web/pgadmin/setup/data_directory.py b/web/pgadmin/setup/data_directory.py index 2335b0790..7a3654b77 100644 --- a/web/pgadmin/setup/data_directory.py +++ b/web/pgadmin/setup/data_directory.py @@ -104,3 +104,19 @@ def create_app_data_directory(config): getpass.getuser(), config.APP_VERSION)) exit(1) + + # Create Kerberos Credential Cache directory (if not present). + try: + _create_directory_if_not_exists(config.KERBEROS_CCACHE_DIR) + except PermissionError as e: + print(FAILED_CREATE_DIR.format(config.KERBEROS_CCACHE_DIR, e)) + print( + "HINT : Create the directory {}, ensure it is writable by\n" + " '{}', and try again, or, create a config_local.py file\n" + " and override the KERBEROS_CCACHE_DIR setting per\n" + " https://www.pgadmin.org/docs/pgadmin4/{}/config_py.html". + format( + config.KERBEROS_CCACHE_DIR, + getpass.getuser(), + config.APP_VERSION)) + exit(1) diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js index 4f89e5bb7..5e4db20a9 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js @@ -13,6 +13,7 @@ import gettext from '../../../../static/js/gettext'; import url_for from '../../../../static/js/url_for'; import _ from 'underscore'; import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper'; +import {fetch_ticket_lifetime} from '../../../../authenticate/static/js/kerberos'; export class BackupDialogWrapper extends DialogWrapper { constructor(dialogContainerSelector, dialogTitle, typeOfDialog, @@ -165,10 +166,29 @@ export class BackupDialogWrapper extends DialogWrapper { ); this.setExtraParameters(selectedTreeNode, treeInfo); + let backupDate = this.view.model.toJSON(); + + if(backupDate.type == 'globals' || backupDate.type == 'server') { + let newPromise = fetch_ticket_lifetime(); + newPromise.then( + function(lifetime) { + if (lifetime < 1800 && lifetime > 0) { + dialog.alertify.warning( + 'You have '+ (Math.round(parseInt(lifetime)/60)).toString() +' minutes left on your ticket - if the dump takes longer than that, it may fail."' + ); + } + }, + function() { + dialog.alertify.warning( + gettext('Please renew your kerberos ticket, it has been expired.') + ); + } + ); + } axios.post( baseUrl, - this.view.model.toJSON() + backupDate ).then(function (res) { if (res.data.success) { dialog.alertify.success(gettext('Backup job created.'), 5); diff --git a/web/pgadmin/tools/debugger/static/js/debugger.js b/web/pgadmin/tools/debugger/static/js/debugger.js index f31a0fc00..460a200bb 100644 --- a/web/pgadmin/tools/debugger/static/js/debugger.js +++ b/web/pgadmin/tools/debugger/static/js/debugger.js @@ -13,11 +13,11 @@ define([ 'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform', 'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils', 'tools/datagrid/static/js/show_query_tool', 'sources/utils', - 'wcdocker', 'pgadmin.browser.frame', + 'pgadmin.authenticate.kerberos', 'wcdocker', 'pgadmin.browser.frame', ], function( gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, CodeMirror, Backform, get_function_arguments, debuggerUtils, showQueryTool, - pgadminUtils, + pgadminUtils, Kerberos ) { var pgTools = pgAdmin.Tools = pgAdmin.Tools || {}, wcDocker = window.wcDocker; @@ -472,8 +472,20 @@ define([ .fail(function(xhr) { try { var err = JSON.parse(xhr.responseText); - if (err.success == 0) { - Alertify.alert(gettext('Debugger Error'), err.errormsg); + if (err.errormsg.search('Ticket expired') !== -1) { + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { + self.start_global_debugger(); + }, + function(error) { + Alertify.alert(gettext('Debugger Error'), error); + } + ); + } else { + if (err.success == 0) { + Alertify.alert(gettext('Debugger Error'), err.errormsg); + } } } catch (e) { console.warn(e.stack || e); diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index b5503255c..631e9d0da 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -51,6 +51,7 @@ define('tools.querytool', [ 'sources/window', 'sources/is_native', 'sources/sqleditor/macro', + 'pgadmin.authenticate.kerberos', 'sources/../bundle/slickgrid', 'pgadmin.file_manager', 'slick.pgadmin.formatters', @@ -65,7 +66,7 @@ define('tools.querytool', [ GeometryViewer, historyColl, queryHist, querySources, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc, - pgWindow, isNative, MacroHandler) { + pgWindow, isNative, MacroHandler, Kerberos) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) return pgAdmin.SqlEditor; @@ -2441,9 +2442,23 @@ define('tools.querytool', [ pgBrowser.report_error(gettext('Error fetching rows - %s.', xhr.statusText), xhr.responseJSON.errormsg, undefined, self.close.bind(self)); } } else { - pgBrowser.Events.trigger( - 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error - ); + if (xhr.responseText.search('Ticket expired') !== -1) { + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { + self.initTransaction(); + }, + function(error) { + pgBrowser.Events.trigger( + 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error + ); + } + ); + } else { + pgBrowser.Events.trigger( + 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error + ); + } } }); }, diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index de9547322..43f6e6de0 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -18,11 +18,13 @@ import select import datetime from collections import deque import psycopg2 -from flask import g, current_app +import threading +from flask import g, current_app, session from flask_babelex import gettext from flask_security import current_user from pgadmin.utils.crypto import decrypt, encrypt from psycopg2.extensions import encodings +from os import environ import config from pgadmin.model import User @@ -38,6 +40,9 @@ from .encoding import get_encoding, configure_driver_encodings from pgadmin.utils import csv from pgadmin.utils.master_password import get_crypt_key from io import StringIO +from pgadmin.utils.constants import KERBEROS + +lock = threading.Lock() _ = gettext @@ -313,6 +318,12 @@ class Connection(BaseConnection): os.environ['PGAPPNAME'] = '{0} - {1}'.format( config.APP_NAME, conn_id) + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS and 'KRB5CCNAME' in session: + lock.acquire() + environ['KRB5CCNAME'] = session['KRB5CCNAME'] + pg_conn = psycopg2.connect( host=manager.local_bind_host if manager.use_ssh_tunnel else manager.host, @@ -340,7 +351,13 @@ class Connection(BaseConnection): if self.async_ == 1: self._wait(pg_conn) + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS: + environ['KRB5CCNAME'] = '' + except psycopg2.Error as e: + environ['KRB5CCNAME'] = '' manager.stop_ssh_tunnel() if e.pgerror: msg = e.pgerror @@ -358,6 +375,11 @@ class Connection(BaseConnection): ) ) return False, msg + finally: + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS and lock.locked(): + lock.release() # Overwrite connection notice attr to support # more than 50 notices at a time @@ -1435,7 +1457,6 @@ Failed to reset the connection to the server due to following error: Args: conn: connection object """ - while True: state = conn.poll() if state == psycopg2.extensions.POLL_OK: diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 96d5b27f6..00daa12ec 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -174,6 +174,7 @@ var webpackShimConfig = { 'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'), 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'), + 'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'), 'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'), 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'), 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'), ^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: [pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication @ 2021-04-26 07:12 Akshay Joshi <[email protected]> parent: Khushboo Vashi <[email protected]> 0 siblings, 1 reply; 5+ messages in thread From: Akshay Joshi @ 2021-04-26 07:12 UTC (permalink / raw) To: Khushboo Vashi <[email protected]>; +Cc: pgadmin-hackers Hi Khushboo I have applied your patch and started testing it in different scenarios. Following are the GUI review comments: - Update the comments about Kerberos support for AUTHENTICATION_SOURCES in config.py. - You will have to create a migration file again. Getting "Error: Multiple head revisions are present for given argument" - Increase the height of the server dialog as after adding "Kerberos Authentication?" switch Connection tab showing scroll bars. - Desktop/Server mode Getting No such file or directory: '/var/lib/pgadmin/krbccache'. KERBEROS_CCACHE_DIR should only be created in Server Mode and AUTHENTICATION_SOURCES is 'kerberos'. - Server Dialog "Kerberos Authentication?" switch control should be enabled only in Server Mode and AUTHENTICATION_SOURCES is 'kerberos'. - "Kerberos Authentication?" switch should be disabled when the server is connected. - In Desktop mode AUTHENTICATION_SOURCES must be '*internal*' doesn't matter what mode is provided in *config.py *or* config_local.py*. In fact, we should create a flag '*authentication_mode*' which will be set after the valid authentication source has been detected/connected. *For example,* the user has provided AUTHENTICATION_SOURCES = ['kerberos', 'internal'], it is unable to connect using kerberos and then the user has provided a valid email and password so we will set '*authentication_mode*' to 'internal' and the rest of the logic will be based on that flag. - Connect to any database server and check backend logs following error is visible: - KeyError: 'KRB5CCNAME' *Solution*: It should not call "kerberos_validate_ticket()" function until AUTHENTICATION_SOURCES is 'kerberos' and Server Mode is true. *AUTHENTICATION_SOURCES = ['kerberos']:* - Kerberos is not set up: Open pgAdmin page, enter email and password two message box popped up one with valid Kerberos error and the second one with "None" as a string. - Similarly, if AUTHENTICATION_SOURCES = ['kerberos', 'internal'] and it is failed to connect using kerberos, then provide an email, and the wrong password two message boxes popped up one with Kerberos error and another with Password error. - In the User Management dialog 'kerberos' should not be visible in the authentication source dropdown. As there is no point creating kerberos user from there. - Add local server(without kerberos) to the browser tree, set "Kerberos Authentication?" to True, try to connect by providing the password it always returns "fe_sendauth: no password supplied" error. If possible can we identify and change the error message? - Add database server where kerberos authentication is ON, make changes in pg_hba.conf with the wrong user name, then try to connect to the database server. The server tries to connect and the spinner is visible and never stops. It should raise a proper error message. There are some other scenarios where entries in pg_hba.conf is wrong. - *Suggestion 1*: As per current implementation even if "Kerberos Authentication?" is set to false the user can connect to the database server by providing any password or blank password. It is difficult for the user to identify it is connected using GSSAPI. I would suggest providing the control in the properties dialog which tells the database server is connected using GSSAPI. - *Suggestion 2*: If it is possible to detect that the database server is connected using Kerberos then we should disable the 'Username' control as for Kerberos both the users (pgadmin user and database user ) must be the same. *Note:- *pgAdmin on OSX not working with Kerberos authentication. Failed with error "Your GSSAPI implementation does not have support for manipulating credential stores directly" Need to document this behavior. *Code review still remains, which I'll be started after the above fixes.* On Wed, Apr 14, 2021 at 2:06 PM Khushboo Vashi < [email protected]> wrote: > Hi, > > Please find the attached patch with some minor improvements. > > Thanks, > Khushboo > > On Wed, Apr 7, 2021 at 11:50 PM Khushboo Vashi < > [email protected]> wrote: > >> Hi, >> >> Please find the attached patch for RM 6158: Support Kerberos >> Authentication - Phase 2. >> This patch includes the support for logging into PostgreSQL servers with >> Kerberos authentication. >> >> Thanks, >> Khushboo >> >> -- *Thanks & Regards* *Akshay Joshi* *pgAdmin Hacker | Principal Software Architect* *EDB Postgres <http://edbpostgres.com>* *Mobile: +91 976-788-8246* ^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: [pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication @ 2021-05-03 09:20 Khushboo Vashi <[email protected]> parent: Akshay Joshi <[email protected]> 0 siblings, 1 reply; 5+ messages in thread From: Khushboo Vashi @ 2021-05-03 09:20 UTC (permalink / raw) To: Akshay Joshi <[email protected]>; +Cc: pgadmin-hackers Hi Akshay, Please find the attached updated patch. Thanks, Khushboo On Mon, Apr 26, 2021 at 12:42 PM Akshay Joshi <[email protected]> wrote: > Hi Khushboo > > I have applied your patch and started testing it in different scenarios. Following > are the GUI review comments: > > - Update the comments about Kerberos support for AUTHENTICATION_SOURCES > in config.py. > > Done. > > - You will have to create a migration file again. Getting "Error: > Multiple head revisions are present for given argument" > > Done. > > - Increase the height of the server dialog as after adding "Kerberos > Authentication?" switch Connection tab showing scroll bars. > > This is the default behaviour of all the dialogues, for example: Table Advanced tab > > - Desktop/Server mode Getting No such file or directory: > '/var/lib/pgadmin/krbccache'. KERBEROS_CCACHE_DIR should only be > created in Server Mode and AUTHENTICATION_SOURCES is 'kerberos'. > > Done > > - Server Dialog "Kerberos Authentication?" switch control should be > enabled only in Server Mode and AUTHENTICATION_SOURCES is 'kerberos'. > > Done > > - "Kerberos Authentication?" switch should be disabled when the server > is connected. > > Even if the user changes the setting when the server is connected, the effect will take place only on reconnection, so I think we can leave it as it is. > > - In Desktop mode AUTHENTICATION_SOURCES must be '*internal*' doesn't > matter what mode is provided in *config.py *or* config_local.py*. In > fact, we should create a flag '*authentication_mode*' which will be > set after the valid authentication source has been detected/connected. *For > example,* the user has provided AUTHENTICATION_SOURCES = ['kerberos', > 'internal'], it is unable to connect using kerberos and then the user has > provided a valid email and password so we will set ' > *authentication_mode*' to 'internal' and the rest of the logic will be > based on that flag. > > This was already taken care of. > > - > > > - Connect to any database server and check backend logs following > error is visible: > - KeyError: 'KRB5CCNAME' *Solution*: It should not call > "kerberos_validate_ticket()" function until AUTHENTICATION_SOURCES is > 'kerberos' and Server Mode is true. > > Fixed. > *AUTHENTICATION_SOURCES = ['kerberos']:* > > - Kerberos is not set up: Open pgAdmin page, enter email and password > two message box popped up one with valid Kerberos error and the second one > with "None" as a string. > > Fixed > > - Similarly, if AUTHENTICATION_SOURCES = ['kerberos', 'internal'] and > it is failed to connect using kerberos, then provide an email, and the > wrong password two message boxes popped up one with Kerberos error and > another with Password error. > > Somehow, I couldn't find the fix for this issue, for now we can ignore this as this will not affect the login process. > > - In the User Management dialog 'kerberos' should not be visible in > the authentication source dropdown. As there is no point creating kerberos > user from there. > > We have provided an option to add manual users for Kerberos also the same as LDAP. > > - Add local server(without kerberos) to the browser tree, set > "Kerberos Authentication?" to True, try to connect by providing the > password it always returns "fe_sendauth: no password supplied" error. If > possible can we identify and change the error message? > > Fixed > > - Add database server where kerberos authentication is ON, make > changes in pg_hba.conf with the wrong user name, then try to connect to the > database server. The server tries to connect and the spinner is visible and > never stops. It should raise a proper error message. There are some other > scenarios where entries in pg_hba.conf is wrong. > > Fixed > > - *Suggestion 1*: As per current implementation even if "Kerberos > Authentication?" is set to false the user can connect to the database > server by providing any password or blank password. It is difficult for the > user to identify it is connected using GSSAPI. I would suggest providing > the control in the properties dialog which tells the database server is > connected using GSSAPI. > > I have removed the old implementation in which the user was able to connect the PostgresQL even if a user has not selected "Kerberos Authentication" but we have a valid kerberos ticket and pg_hba is configured to support it. So, now users can get the idea about the connection through The "Kerberos authentication" flag displayed on the properties tab. > > - *Suggestion 2*: If it is possible to detect that the database server > is connected using Kerberos then we should disable the 'Username' control > as for Kerberos both the users (pgadmin user and database user ) must be > the same. > > > *Note:- *pgAdmin on OSX not working with Kerberos authentication. Failed > with error "Your GSSAPI implementation does not have support for > manipulating credential stores directly" Need to document this behavior. > Thanks, khushboo > > *Code review still remains, which I'll be started after the above fixes.* > > On Wed, Apr 14, 2021 at 2:06 PM Khushboo Vashi < > [email protected]> wrote: > >> Hi, >> >> Please find the attached patch with some minor improvements. >> >> Thanks, >> Khushboo >> >> On Wed, Apr 7, 2021 at 11:50 PM Khushboo Vashi < >> [email protected]> wrote: >> >>> Hi, >>> >>> Please find the attached patch for RM 6158: Support Kerberos >>> Authentication - Phase 2. >>> This patch includes the support for logging into PostgreSQL servers with >>> Kerberos authentication. >>> >>> Thanks, >>> Khushboo >>> >>> > > -- > *Thanks & Regards* > *Akshay Joshi* > *pgAdmin Hacker | Principal Software Architect* > *EDB Postgres <http://edbpostgres.com>* > > *Mobile: +91 976-788-8246* > Attachments: [application/octet-stream] RM_6158_v2.patch (51.1K, 3-RM_6158_v2.patch) download | inline diff: diff --git a/web/config.py b/web/config.py index a44fddc2e..2643ef19e 100644 --- a/web/config.py +++ b/web/config.py @@ -634,6 +634,9 @@ KRB_KTNAME = '<KRB5_KEYTAB_FILE>' KRB_AUTO_CREATE_USER = True +KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache') + + ########################################################################## # Local config settings ########################################################################## diff --git a/web/migrations/versions/d0bc9f32b2b9_.py b/web/migrations/versions/d0bc9f32b2b9_.py new file mode 100644 index 000000000..266b6d899 --- /dev/null +++ b/web/migrations/versions/d0bc9f32b2b9_.py @@ -0,0 +1,28 @@ + +"""empty message + +Revision ID: d0bc9f32b2b9 +Revises: c6974f64df08 +Create Date: 2021-04-27 12:40:08.899712 + +""" +from alembic import op +import sqlalchemy as sa +from pgadmin.model import db + +# revision identifiers, used by Alembic. +revision = 'd0bc9f32b2b9' +down_revision = 'c6974f64df08' +branch_labels = None +depends_on = None + + +def upgrade(): + db.engine.execute( + 'ALTER TABLE server ADD COLUMN kerberos_conn INTEGER DEFAULT 0' + ) + + +def downgrade(): + # pgAdmin only upgrades, downgrade not implemented. + pass diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index 9166c2ffd..40c76b2b3 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -13,10 +13,13 @@ import flask import pickle from flask import current_app, flash, Response, request, url_for,\ render_template -from flask_security import current_user +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 import session @@ -34,7 +37,9 @@ class AuthenticateModule(PgAdminModule): def get_exposed_url_endpoints(self): return ['authenticate.login', 'authenticate.kerberos_login', - 'authenticate.kerberos_logout'] + 'authenticate.kerberos_logout', + 'authenticate.kerberos_update_ticket', + 'authenticate.kerberos_validate_ticket'] blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='') @@ -55,6 +60,12 @@ def kerberos_login(): @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'), )) @@ -165,6 +176,8 @@ class AuthSourceManager(): 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) @@ -173,11 +186,13 @@ class AuthSourceManager(): # 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 and (hasattr(msg, 'status') and - msg.status == '401 UNAUTHORIZED') or \ - (source.get_source_name() == KERBEROS and - request.method == 'GET'): - break + 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) @@ -224,3 +239,58 @@ def init_app(app): AuthSourceRegistry.load_auth_sources() return auth_sources + + [email protected]("/kerberos/update_ticket", + endpoint="kerberos_update_ticket", methods=["GET"]) [email protected] +@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) + + [email protected]("/kerberos/validate_ticket", + endpoint="kerberos_validate_ticket", methods=["GET"]) [email protected] +@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/kerberos.py b/web/pgadmin/authenticate/kerberos.py index 57aa1e0f0..2f8fd0d6e 100644 --- a/web/pgadmin/authenticate/kerberos.py +++ b/web/pgadmin/authenticate/kerberos.py @@ -10,7 +10,7 @@ """A blueprint module implementing the Spnego/Kerberos authentication.""" import base64 -from os import environ +from os import environ, path from werkzeug.datastructures import Headers from flask_babelex import gettext @@ -128,19 +128,37 @@ class KerberosAuthentication(BaseAuthentication): if out_token and not context.complete: return False, out_token if context.complete: + deleg_creds = context.delegated_creds + if not hasattr(deleg_creds, 'name'): + error_msg = gettext('Delegated credentials not supplied.') + current_app.logger.error(error_msg) + return False, Exception(error_msg) + try: + cache_file_path = path.join( + config.KERBEROS_CCACHE_DIR, 'pgadmin_cache_{0}'.format( + deleg_creds.name) + ) + CCACHE = 'FILE:{0}'.format(cache_file_path) + store = {'ccache': CCACHE} + deleg_creds.store(store, overwrite=True, set_default=True) + session['KRB5CCNAME'] = CCACHE + except Exception as e: + current_app.logger.exception(e) + return False, e + return True, context else: return False, None def negotiate_end(self, context): - # Free gss_cred_id_t + # Free Delegated Credentials 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.""" + """Add the kerberos user to the internal SQLite database.""" username = str(username) if config.KRB_AUTO_CREATE_USER: user = User.query.filter_by( diff --git a/web/pgadmin/authenticate/static/js/kerberos.js b/web/pgadmin/authenticate/static/js/kerberos.js new file mode 100644 index 000000000..64373369c --- /dev/null +++ b/web/pgadmin/authenticate/static/js/kerberos.js @@ -0,0 +1,59 @@ +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') + ) + .then(function(response){ + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response); + } else { + return Promise.reject(new Error(response.statusText)); + } + }); +} + +function fetch_ticket_lifetime () { + // Fetch the Kerberos ticket lifetime left + + return fetch(url_for('authenticate.kerberos_validate_ticket') + ) + .then( + function(response){ + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + return Promise.reject(new Error(response.statusText)); + } + } + ) + .then(function(response){ + let ticket_lifetime = response.data.ticket_lifetime; + if (ticket_lifetime > 0) { + return Promise.resolve(ticket_lifetime); + } else { + return Promise.reject(); + } + }); + +} + +function validate_kerberos_ticket() { + // Ping pgAdmin server every 10 seconds + // to fetch the Kerberos ticket lifetime left + if (userInfo['current_auth_source'] != pgConst['KERBEROS']) return; + + return setInterval(function() { + let newPromise = fetch_ticket_lifetime(); + newPromise.then( + function() { + return; + }, + fetch_ticket + ); + }, 10000); +} + +export {fetch_ticket, validate_kerberos_ticket, fetch_ticket_lifetime}; diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 5fc7de64f..300625c5e 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -50,7 +50,7 @@ from pgadmin.utils.master_password import validate_master_password, \ set_crypt_key, process_masterpass_disabled from pgadmin.model import User from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\ - INTERNAL, KERBEROS + INTERNAL, KERBEROS, LDAP try: from flask_security.views import default_render_json @@ -197,7 +197,8 @@ class BrowserModule(PgAdminModule): for name, script in [ [PGADMIN_BROWSER, 'js/browser'], ['pgadmin.browser.endpoints', 'js/endpoints'], - ['pgadmin.browser.error', 'js/error'] + ['pgadmin.browser.error', 'js/error'], + ['pgadmin.browser.constants', 'js/constants'] ]: scripts.append({ 'name': name, @@ -864,6 +865,18 @@ def exposed_urls(): ) [email protected]("/js/constants.js") [email protected] +def app_constants(): + return make_response( + render_template('browser/js/constants.js', + INTERNAL=INTERNAL, + LDAP=LDAP, + KERBEROS=KERBEROS), + 200, {'Content-Type': MIMETYPE_APP_JS} + ) + + @blueprint.route("/js/error.js") @pgCSRFProtect.exempt @login_required diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 105e90c8d..dc16a5de8 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -253,7 +253,8 @@ class ServerModule(sg.ServerGroupPluginModule): errmsg=errmsg, user_id=server.user_id, user_name=server.username, - shared=server.shared + shared=server.shared, + is_kerberos_conn=bool(server.kerberos_conn), ) @property @@ -547,7 +548,8 @@ class ServerNode(PGChildNodeView): if server.tunnel_password is not None else False, errmsg=errmsg, user_name=server.username, - shared=server.shared + shared=server.shared, + is_kerberos_conn=bool(server.kerberos_conn) ) ) @@ -614,7 +616,8 @@ class ServerNode(PGChildNodeView): if server.tunnel_password is not None else False, errmsg=errmsg, shared=server.shared, - user_name=server.username + user_name=server.username, + is_kerberos_conn=bool(server.kerberos_conn) ), ) @@ -721,7 +724,8 @@ class ServerNode(PGChildNodeView): 'tunnel_username': 'tunnel_username', 'tunnel_authentication': 'tunnel_authentication', 'tunnel_identity_file': 'tunnel_identity_file', - 'shared': 'shared' + 'shared': 'shared', + 'kerberos_conn': 'kerberos_conn', } disp_lbl = { @@ -985,7 +989,8 @@ class ServerNode(PGChildNodeView): 'tunnel_username': tunnel_username, 'tunnel_identity_file': server.tunnel_identity_file if server.tunnel_identity_file else None, - 'tunnel_authentication': tunnel_authentication + 'tunnel_authentication': tunnel_authentication, + 'kerberos_conn': bool(server.kerberos_conn), } return ajax_response(response) @@ -1072,7 +1077,8 @@ class ServerNode(PGChildNodeView): tunnel_authentication=data.get('tunnel_authentication', 0), tunnel_identity_file=data.get('tunnel_identity_file', None), shared=data.get('shared', None), - passfile=data.get('passfile', None) + passfile=data.get('passfile', None), + kerberos_conn=1 if data.get('kerberos_conn', False) else 0, ) db.session.add(server) db.session.commit() @@ -1154,7 +1160,8 @@ class ServerNode(PGChildNodeView): else 'pg', version=manager.version if manager and manager.version - else None + else None, + is_kerberos_conn=bool(server.kerberos_conn), ) ) @@ -1348,7 +1355,7 @@ class ServerNode(PGChildNodeView): except Exception as e: current_app.logger.exception(e) return internal_server_error(errormsg=str(e)) - if 'password' not in data: + if 'password' not in data and server.kerberos_conn is False: conn_passwd = getattr(conn, 'password', None) if conn_passwd is None and not server.save_password and \ server.passfile is None and server.service is None: @@ -1400,6 +1407,9 @@ class ServerNode(PGChildNodeView): "Could not connect to server(#{0}) - '{1}'.\nError: {2}" .format(server.id, server.name, errmsg) ) + if errmsg.find('Ticket expired') != -1: + return internal_server_error(errmsg) + return self.get_response_for_password(server, 401, True, True, errmsg) else: @@ -1467,6 +1477,7 @@ class ServerNode(PGChildNodeView): 'is_password_saved': bool(server.save_password), 'is_tunnel_password_saved': True if server.tunnel_password is not None else False, + 'is_kerberos_conn': bool(server.kerberos_conn), } ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py index 60af1de42..4b1d7308d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py @@ -490,6 +490,7 @@ class DatabaseView(PGChildNodeView): did, errmsg ) ) + return internal_server_error(errmsg) else: current_app.logger.info( diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index c53f04429..01ab89c50 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -10,9 +10,10 @@ define('pgadmin.node.database', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'sources/utils', 'sources/pgadmin', 'pgadmin.browser.utils', - 'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.collection', + 'pgadmin.alertifyjs', 'pgadmin.backform', + 'pgadmin.authenticate.kerberos', 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege', 'pgadmin.browser.server.variable', -], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform) { +], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform, Kerberos) { if (!pgBrowser.Nodes['coll-database']) { pgBrowser.Nodes['coll-database'] = @@ -556,24 +557,39 @@ define('pgadmin.node.database', [ onFailure = function( xhr, status, error, _model, _data, _tree, _item, _status ) { - if (!_status) { - tree.setInode(_item); - tree.addIcon(_item, {icon: 'icon-database-not-connected'}); - } - - Alertify.pgNotifier('error', xhr, error, function(msg) { - setTimeout(function() { - if (msg == 'CRYPTKEY_SET') { + if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) { + tree.addIcon(_item, {icon: 'icon-server-connecting'}); + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { connect_to_database(_model, _data, _tree, _item, _wasConnected); - } else { - Alertify.dlgServerPass( - gettext('Connect to database'), - msg, _model, _data, _tree, _item, _status, - onSuccess, onFailure, onCancel - ).resizeTo(); + }, + function(error) { + tree.setInode(_item); + tree.addIcon(_item, {icon: 'icon-database-not-connected'}); + Alertify.pgNotifier(error, xhr, gettext('Connect to database.')); } - }, 100); - }); + ); + } else { + if (!_status) { + tree.setInode(_item); + tree.addIcon(_item, {icon: 'icon-database-not-connected'}); + } + + Alertify.pgNotifier('error', xhr, error, function(msg) { + setTimeout(function() { + if (msg == 'CRYPTKEY_SET') { + connect_to_database(_model, _data, _tree, _item, _wasConnected); + } else { + Alertify.dlgServerPass( + gettext('Connect to database'), + msg, _model, _data, _tree, _item, _status, + onSuccess, onFailure, onCancel + ).resizeTo(); + } + }, 100); + }); + } }, onSuccess = function( res, model, _data, _tree, _item, _connected @@ -640,6 +656,7 @@ define('pgadmin.node.database', [ if (xhr.status === 410) { error = gettext('Error: Object not found - %s.', error); } + return onFailure( xhr, status, error, obj, data, tree, item, wasConnected ); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index ab95d6d89..fd525d763 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -13,11 +13,13 @@ define('pgadmin.node.server', [ 'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user', 'pgadmin.alertifyjs', 'pgadmin.backform', 'sources/browser/server_groups/servers/model_validation', + 'pgadmin.authenticate.kerberos', + 'pgadmin.browser.constants', 'pgadmin.browser.server.privilege', ], function( gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, supported_servers, current_user, Alertify, Backform, - modelValidation + modelValidation, Kerberos, pgConst, ) { if (!pgBrowser.Nodes['server']) { @@ -904,20 +906,36 @@ define('pgadmin.node.server', [ } }, }), + },{ + id: 'kerberos_conn', label: gettext('Kerberos authentication?'), type: 'switch', + group: gettext('Connection'), 'options': { + 'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini', + }, disabled: function() { + if (current_user['current_auth_source'] != pgConst['KERBEROS']) + return true; + return false; + }, },{ id: 'password', label: gettext('Password'), type: 'password', maxlength: null, - group: gettext('Connection'), control: 'input', mode: ['create'], deps: ['connect_now'], + group: gettext('Connection'), control: 'input', mode: ['create'], + deps: ['connect_now', 'kerberos_conn'], visible: function(model) { return model.get('connect_now') && model.isNew(); }, + disabled: function(model) { + if (model.get('kerberos_conn')) + return true; + + return false; + }, },{ id: 'save_password', controlLabel: gettext('Save password?'), type: 'checkbox', group: gettext('Connection'), mode: ['create'], - deps: ['connect_now'], visible: function(model) { + deps: ['connect_now', 'kerberos_conn'], visible: function(model) { return model.get('connect_now') && model.isNew(); }, - disabled: function() { - if (!current_user.allow_save_password) + disabled: function(model) { + if (!current_user.allow_save_password || model.get('kerberos_conn')) return true; return false; @@ -1279,19 +1297,32 @@ define('pgadmin.node.server', [ } } - - Alertify.pgNotifier('error', xhr, error, function(msg) { - setTimeout(function() { - if (msg == 'CRYPTKEY_SET') { + if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) { + tree.addIcon(_item, {icon: 'icon-server-connecting'}); + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { connect_to_server(_node, _data, _tree, _item, _wasConnected); - } else { - Alertify.dlgServerPass( - gettext('Connect to Server'), - msg, _node, _data, _tree, _item, _wasConnected - ).resizeTo(); + }, + function() { + tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + Alertify.pgNotifier('Connection error', xhr, gettext('Connect to server.')); } - }, 100); - }); + ); + } else { + Alertify.pgNotifier('error', xhr, error, function(msg) { + setTimeout(function() { + if (msg == 'CRYPTKEY_SET') { + connect_to_server(_node, _data, _tree, _item, _wasConnected); + } else { + Alertify.dlgServerPass( + gettext('Connect to Server'), + msg, _node, _data, _tree, _item, _wasConnected + ).resizeTo(); + } + }, 100); + }); + } }, onSuccess = function(res, node, _data, _tree, _item, _wasConnected) { if (res && res.data) { diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 4ffb5ee5a..bf44aa6f4 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -12,19 +12,22 @@ define('pgadmin.browser', [ 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', 'sources/check_node_visibility', './toolbar', 'pgadmin.help', - 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.browser.utils', - 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', + 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.authenticate.kerberos', + 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', + 'jquery.acitree', 'pgadmin.browser.preferences', 'pgadmin.browser.messages', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', 'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', - 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment', + 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', + 'jquery.acifragment', ], function( tree, gettext, url_for, require, $, _, Bootstrap, pgAdmin, Alertify, codemirror, - checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow + checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow, + Kerberos ) { window.jQuery = window.$ = $; // Some scripts do export their object in the window only. @@ -38,6 +41,8 @@ define('pgadmin.browser', [ csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); + Kerberos.validate_kerberos_ticket(); + var panelEvents = {}; panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() { if (this.isVisible()) { diff --git a/web/pgadmin/browser/templates/browser/js/constants.js b/web/pgadmin/browser/templates/browser/js/constants.js new file mode 100644 index 000000000..6a63d6ed9 --- /dev/null +++ b/web/pgadmin/browser/templates/browser/js/constants.js @@ -0,0 +1,17 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + + +define('pgadmin.browser.constants', [], function() { + return { + 'INTERNAL': '{{ INTERNAL }}', + 'LDAP': '{{ LDAP }}', + 'KERBEROS': '{{ KERBEROS }}' + } +}); diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py index f31e983ff..6b61dc1d0 100644 --- a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py +++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py @@ -12,6 +12,7 @@ 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 werkzeug.datastructures import Headers class KerberosLoginMockTestCase(BaseTestGenerator): @@ -30,6 +31,11 @@ class KerberosLoginMockTestCase(BaseTestGenerator): auth_source=['kerberos'], auto_create_user=True, flag=2 + )), + ('Spnego/Kerberos Update Ticket', dict( + auth_source=['kerberos'], + auto_create_user=True, + flag=3 )) ] @@ -54,8 +60,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator): self.skipTest( "Can not run Kerberos Authentication in the Desktop mode." ) - self.test_authorized() + elif self.flag == 3: + if app_config.SERVER_MODE is False: + self.skipTest( + "Can not run Kerberos Authentication in the Desktop mode." + ) + self.test_update_ticket() def test_unauthorized(self): """ @@ -73,13 +84,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator): passed on to the routed method. """ - class delCrads: - def __init__(self): - self.initiator_name = '[email protected]' - del_crads = delCrads() - - AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock( - return_value=[True, del_crads]) + del_crads = self.mock_negotiate_start() res = self.tester.login(None, None, True, @@ -89,6 +94,33 @@ class KerberosLoginMockTestCase(BaseTestGenerator): respdata = 'Gravatar image for %s' % del_crads.initiator_name self.assertTrue(respdata in res.data.decode('utf8')) + def mock_negotiate_start(self): + class delCrads: + def __init__(self): + self.initiator_name = '[email protected]' + + del_crads = delCrads() + + 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') + self.assertEqual(response.status_code, 401) + self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate') + + # When we send the Kerberos Ticket, it should return success + del_crads = self.mock_negotiate_start() + + krb_token = Headers({}) + krb_token['Authorization'] = 'Negotiate CTOKEN' + + response = self.tester.get('/authenticate/kerberos/update_ticket', + headers=krb_token) + self.assertEqual(response.status_code, 200) + def tearDown(self): self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py index ef6cfc3f2..25e0a2a9e 100644 --- a/web/pgadmin/misc/bgprocess/processes.py +++ b/web/pgadmin/misc/bgprocess/processes.py @@ -24,10 +24,11 @@ import logging from pgadmin.utils import u_encode, file_quote, fs_encoding, \ get_complete_file_path, get_storage_directory, IS_WIN from pgadmin.browser.server_groups.servers.utils import does_server_exists +from pgadmin.utils.constants import KERBEROS import pytz from dateutil import parser -from flask import current_app +from flask import current_app, session from flask_babelex import gettext as _ from flask_security import current_user @@ -278,13 +279,16 @@ class BatchProcess(object): env['PROCID'] = self.id env['OUTDIR'] = self.log_dir env['PGA_BGP_FOREGROUND'] = "1" + if config.SERVER_MODE and session and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS: + env['KRB5CCNAME'] = session['KRB5CCNAME'] if self.env: env.update(self.env) if cb is not None: cb(env) - if os.name == 'nt': DETACHED_PROCESS = 0x00000008 from subprocess import CREATE_NEW_PROCESS_GROUP diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index d849b8c26..edfa7a49f 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy # ########################################################################## -SCHEMA_VERSION = 28 +SCHEMA_VERSION = 29 ########################################################################## # @@ -184,6 +184,7 @@ class Server(db.Model): tunnel_identity_file = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(db.String(64), nullable=True) shared = db.Column(db.Boolean(), nullable=False) + kerberos_conn = db.Column(db.Boolean(), nullable=False) @property def serialize(self): diff --git a/web/pgadmin/setup/data_directory.py b/web/pgadmin/setup/data_directory.py index 2335b0790..c5778889f 100644 --- a/web/pgadmin/setup/data_directory.py +++ b/web/pgadmin/setup/data_directory.py @@ -9,6 +9,8 @@ import os import getpass +from flask import current_app +from pgadmin.utils.constants import KERBEROS FAILED_CREATE_DIR = \ "ERROR : Failed to create the directory {}:\n {}" @@ -104,3 +106,20 @@ def create_app_data_directory(config): getpass.getuser(), config.APP_VERSION)) exit(1) + + # Create Kerberos Credential Cache directory (if not present). + if config.SERVER_MODE and KERBEROS in config.AUTHENTICATION_SOURCES: + try: + _create_directory_if_not_exists(config.KERBEROS_CCACHE_DIR) + except PermissionError as e: + print(FAILED_CREATE_DIR.format(config.KERBEROS_CCACHE_DIR, e)) + print( + "HINT : Create the directory {}, ensure it is writable by\n" + "'{}', and try again, or, create a config_local.py file\n" + " and override the KERBEROS_CCACHE_DIR setting per\n" + " https://www.pgadmin.org/docs/pgadmin4/{}/config_py.html". + format( + config.KERBEROS_CCACHE_DIR, + getpass.getuser(), + config.APP_VERSION)) + exit(1) diff --git a/web/pgadmin/templates/base.html b/web/pgadmin/templates/base.html index 7d3743543..ce375ec44 100644 --- a/web/pgadmin/templates/base.html +++ b/web/pgadmin/templates/base.html @@ -48,6 +48,7 @@ 'pgadmin.browser.utils': "{{ url_for('browser.index') }}" + "js/utils", 'pgadmin.browser.endpoints': "{{ url_for('browser.index') }}" + "js/endpoints", 'pgadmin.browser.messages': "{{ url_for('browser.index') }}" + "js/messages", + 'pgadmin.browser.constants': "{{ url_for('browser.index') }}" + "js/constants", 'pgadmin.server.supported_servers': "{{ url_for('browser.index') }}" + "server/supported_servers", 'pgadmin.user_management.current_user': "{{ url_for('user_management.index') }}" + "current_user", 'translations': "{{ url_for('tools.index') }}" + "translations" diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js index 4f89e5bb7..7e0ddd63f 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js @@ -13,6 +13,8 @@ import gettext from '../../../../static/js/gettext'; import url_for from '../../../../static/js/url_for'; import _ from 'underscore'; import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper'; +import {fetch_ticket_lifetime} from '../../../../authenticate/static/js/kerberos'; +import userInfo from 'pgadmin.user_management.current_user'; export class BackupDialogWrapper extends DialogWrapper { constructor(dialogContainerSelector, dialogTitle, typeOfDialog, @@ -165,10 +167,29 @@ export class BackupDialogWrapper extends DialogWrapper { ); this.setExtraParameters(selectedTreeNode, treeInfo); + let backupDate = this.view.model.toJSON(); + + if(userInfo['auth_sources'] == 'KERBEROS' && (backupDate.type == 'globals' || backupDate.type == 'server')) { + let newPromise = fetch_ticket_lifetime(); + newPromise.then( + function(lifetime) { + if (lifetime < 1800 && lifetime > 0) { + dialog.alertify.warning( + 'You have '+ (Math.round(parseInt(lifetime)/60)).toString() +' minutes left on your ticket - if the dump takes longer than that, it may fail."' + ); + } + }, + function() { + dialog.alertify.warning( + gettext('Please renew your kerberos ticket, it has been expired.') + ); + } + ); + } axios.post( baseUrl, - this.view.model.toJSON() + backupDate ).then(function (res) { if (res.data.success) { dialog.alertify.success(gettext('Backup job created.'), 5); diff --git a/web/pgadmin/tools/debugger/static/js/debugger.js b/web/pgadmin/tools/debugger/static/js/debugger.js index f31a0fc00..460a200bb 100644 --- a/web/pgadmin/tools/debugger/static/js/debugger.js +++ b/web/pgadmin/tools/debugger/static/js/debugger.js @@ -13,11 +13,11 @@ define([ 'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform', 'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils', 'tools/datagrid/static/js/show_query_tool', 'sources/utils', - 'wcdocker', 'pgadmin.browser.frame', + 'pgadmin.authenticate.kerberos', 'wcdocker', 'pgadmin.browser.frame', ], function( gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, CodeMirror, Backform, get_function_arguments, debuggerUtils, showQueryTool, - pgadminUtils, + pgadminUtils, Kerberos ) { var pgTools = pgAdmin.Tools = pgAdmin.Tools || {}, wcDocker = window.wcDocker; @@ -472,8 +472,20 @@ define([ .fail(function(xhr) { try { var err = JSON.parse(xhr.responseText); - if (err.success == 0) { - Alertify.alert(gettext('Debugger Error'), err.errormsg); + if (err.errormsg.search('Ticket expired') !== -1) { + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { + self.start_global_debugger(); + }, + function(error) { + Alertify.alert(gettext('Debugger Error'), error); + } + ); + } else { + if (err.success == 0) { + Alertify.alert(gettext('Debugger Error'), err.errormsg); + } } } catch (e) { console.warn(e.stack || e); diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 28d989f47..85615c75c 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -51,6 +51,7 @@ define('tools.querytool', [ 'sources/window', 'sources/is_native', 'sources/sqleditor/macro', + 'pgadmin.authenticate.kerberos', 'sources/../bundle/slickgrid', 'pgadmin.file_manager', 'slick.pgadmin.formatters', @@ -65,7 +66,7 @@ define('tools.querytool', [ GeometryViewer, historyColl, queryHist, querySources, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc, - pgWindow, isNative, MacroHandler) { + pgWindow, isNative, MacroHandler, Kerberos) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) return pgAdmin.SqlEditor; @@ -2451,9 +2452,23 @@ define('tools.querytool', [ pgBrowser.report_error(gettext('Error fetching rows - %s.', xhr.statusText), xhr.responseJSON.errormsg, undefined, self.close.bind(self)); } } else { - pgBrowser.Events.trigger( - 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error - ); + if (xhr.responseText.search('Ticket expired') !== -1) { + let fetchTicket = Kerberos.fetch_ticket(); + fetchTicket.then( + function() { + self.initTransaction(); + }, + function(error) { + pgBrowser.Events.trigger( + 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error + ); + } + ); + } else { + pgBrowser.Events.trigger( + 'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error + ); + } } }); }, diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py index ebfba540b..5d56081ba 100644 --- a/web/pgadmin/tools/user_management/__init__.py +++ b/web/pgadmin/tools/user_management/__init__.py @@ -25,7 +25,7 @@ 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, INTERNAL,\ - SUPPORTED_AUTH_SOURCES, KERBEROS + SUPPORTED_AUTH_SOURCES, KERBEROS, LDAP from pgadmin.utils.validation_utils import validate_email from pgadmin.model import db, Role, User, UserPreference, Server, \ ServerGroup, Process, Setting, roles_users, SharedServer @@ -157,7 +157,6 @@ def script(): @pgCSRFProtect.exempt @login_required def current_user_info(): - return Response( response=render_template( "user_management/js/current_user.js", @@ -176,7 +175,9 @@ def current_user_info(): allow_save_tunnel_password='true' if config.ALLOW_SAVE_TUNNEL_PASSWORD and session[ 'allow_save_password'] else 'false', - auth_sources=config.AUTHENTICATION_SOURCES + auth_sources=config.AUTHENTICATION_SOURCES, + current_auth_source=session['_auth_source_manager_obj'][ + 'current_source'] if config.SERVER_MODE is True else INTERNAL ), status=200, mimetype=MIMETYPE_APP_JS 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 cfa6e5a26..e436f4948 100644 --- a/web/pgadmin/tools/user_management/static/js/user_management.js +++ b/web/pgadmin/tools/user_management/static/js/user_management.js @@ -10,11 +10,11 @@ define([ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs', 'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node', 'pgadmin.backform', - 'pgadmin.user_management.current_user', 'sources/utils', + 'pgadmin.user_management.current_user', 'sources/utils', 'pgadmin.browser.constants', 'backgrid.select.all', 'backgrid.filter', ], function( gettext, url_for, $, _, alertify, pgBrowser, Backbone, Backgrid, Backform, - pgNode, pgBackform, userInfo, commonUtils, + pgNode, pgBackform, userInfo, commonUtils, pgConst, ) { // if module is already initialized, refer to that. @@ -25,7 +25,9 @@ define([ var USERURL = url_for('user_management.users'), ROLEURL = url_for('user_management.roles'), SOURCEURL = url_for('user_management.auth_sources'), - DEFAULT_AUTH_SOURCE = 'internal', + DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'], + LDAP = pgConst['LDAP'], + KERBEROS = pgConst['KERBEROS'], 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({ @@ -589,7 +591,17 @@ define([ } } else { if (!!this.get('username') && this.collection.nonFilter.where({ - 'username': this.get('username'), 'auth_source': 'ldap', + 'username': this.get('username'), 'auth_source': LDAP, + }).length > 1) { + errmsg = gettext('The username %s already exists.', + 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': KERBEROS, }).length > 1) { errmsg = gettext('The username %s already exists.', this.get('username') @@ -1041,7 +1053,7 @@ define([ saveUser: function(m) { var d = m.toJSON(true); - if((m.isNew() && m.get('auth_source') == 'ldap' && (!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('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/tools/user_management/templates/user_management/js/current_user.js b/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js index 2516dc425..bcb02f1ab 100644 --- a/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js +++ b/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js @@ -15,6 +15,7 @@ define('pgadmin.user_management.current_user', [], function() { 'name': '{{ name }}', 'allow_save_password': {{ allow_save_password }}, 'allow_save_tunnel_password': {{ allow_save_tunnel_password }}, - 'auth_sources': {{ auth_sources }} + 'auth_sources': {{ auth_sources }}, + 'current_auth_source': '{{ current_auth_source }}' } }); diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index be824da1e..3baa61fac 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -18,11 +18,13 @@ import select import datetime from collections import deque import psycopg2 -from flask import g, current_app +import threading +from flask import g, current_app, session from flask_babelex import gettext from flask_security import current_user from pgadmin.utils.crypto import decrypt, encrypt from psycopg2.extensions import encodings +from os import environ import config from pgadmin.model import User @@ -38,6 +40,9 @@ from .encoding import get_encoding, configure_driver_encodings from pgadmin.utils import csv from pgadmin.utils.master_password import get_crypt_key from io import StringIO +from pgadmin.utils.constants import KERBEROS + +lock = threading.Lock() _ = gettext @@ -313,6 +318,13 @@ class Connection(BaseConnection): os.environ['PGAPPNAME'] = '{0} - {1}'.format( config.APP_NAME, conn_id) + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS and 'KRB5CCNAME' in session\ + and manager.kerberos_conn: + lock.acquire() + environ['KRB5CCNAME'] = session['KRB5CCNAME'] + pg_conn = psycopg2.connect( host=manager.local_bind_host if manager.use_ssh_tunnel else manager.host, @@ -340,7 +352,13 @@ class Connection(BaseConnection): if self.async_ == 1: self._wait(pg_conn) + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS: + environ['KRB5CCNAME'] = '' + except psycopg2.Error as e: + environ['KRB5CCNAME'] = '' manager.stop_ssh_tunnel() if e.pgerror: msg = e.pgerror @@ -358,6 +376,11 @@ class Connection(BaseConnection): ) ) return False, msg + finally: + if config.SERVER_MODE and \ + session['_auth_source_manager_obj']['current_source'] == \ + KERBEROS and lock.locked(): + lock.release() # Overwrite connection notice attr to support # more than 50 notices at a time @@ -1438,7 +1461,6 @@ Failed to reset the connection to the server due to following error: Args: conn: connection object """ - while True: state = conn.poll() if state == psycopg2.extensions.POLL_OK: diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py index 7e1199a7d..8c16c8ec3 100644 --- a/web/pgadmin/utils/driver/psycopg2/server_manager.py +++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py @@ -105,6 +105,7 @@ class ServerManager(object): self.tunnel_identity_file = None self.tunnel_password = None + self.kerberos_conn = server.kerberos_conn for con in self.connections: self.connections[con]._release() diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py index 629eec941..f962684ff 100644 --- a/web/pgadmin/utils/master_password.py +++ b/web/pgadmin/utils/master_password.py @@ -34,7 +34,7 @@ def get_crypt_key(): 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']\ + session['_auth_source_manager_obj']['current_source']\ == KERBEROS: return True, session['kerberos_key'] if 'kerberos_key' in session \ else None diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 96d5b27f6..074b25806 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -174,11 +174,13 @@ var webpackShimConfig = { 'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'), 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'), + 'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'), 'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'), 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'), 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'), 'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'), 'pgadmin.browser.endpoints': '/browser/js/endpoints', + 'pgadmin.browser.constants': '/browser/js/constants', 'pgadmin.browser.error': path.join(__dirname, './pgadmin/browser/static/js/error'), 'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'), 'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'), @@ -300,6 +302,7 @@ var webpackShimConfig = { 'pgadmin.browser.messages', 'pgadmin.browser.utils', 'pgadmin.server.supported_servers', + 'pgadmin.browser.constants', ], // Define list of pgAdmin common libraries to bundle them separately // into commons JS from app.bundle.js ^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: [pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication @ 2021-05-03 10:41 Akshay Joshi <[email protected]> parent: Khushboo Vashi <[email protected]> 0 siblings, 0 replies; 5+ messages in thread From: Akshay Joshi @ 2021-05-03 10:41 UTC (permalink / raw) To: Khushboo Vashi <[email protected]>; +Cc: pgadmin-hackers Thanks, patch applied. On Mon, May 3, 2021 at 2:50 PM Khushboo Vashi < [email protected]> wrote: > Hi Akshay, > > Please find the attached updated patch. > > Thanks, > Khushboo > > On Mon, Apr 26, 2021 at 12:42 PM Akshay Joshi < > [email protected]> wrote: > >> Hi Khushboo >> >> I have applied your patch and started testing it in different scenarios. Following >> are the GUI review comments: >> >> - Update the comments about Kerberos support for AUTHENTICATION_SOURCES >> in config.py. >> >> Done. > >> >> - You will have to create a migration file again. Getting "Error: >> Multiple head revisions are present for given argument" >> >> Done. > >> >> - Increase the height of the server dialog as after adding "Kerberos >> Authentication?" switch Connection tab showing scroll bars. >> >> This is the default behaviour of all the dialogues, for example: Table > Advanced tab > >> >> - Desktop/Server mode Getting No such file or directory: >> '/var/lib/pgadmin/krbccache'. KERBEROS_CCACHE_DIR should only be >> created in Server Mode and AUTHENTICATION_SOURCES is 'kerberos'. >> >> Done > >> >> - Server Dialog "Kerberos Authentication?" switch control should be >> enabled only in Server Mode and AUTHENTICATION_SOURCES is 'kerberos'. >> >> Done > >> >> - "Kerberos Authentication?" switch should be disabled when the >> server is connected. >> >> Even if the user changes the setting when the server is connected, the > effect will take place only on reconnection, so I think we can leave it as > it is. > >> >> - In Desktop mode AUTHENTICATION_SOURCES must be '*internal*' doesn't >> matter what mode is provided in *config.py *or* config_local.py*. In >> fact, we should create a flag '*authentication_mode*' which will be >> set after the valid authentication source has been detected/connected. *For >> example,* the user has provided AUTHENTICATION_SOURCES = >> ['kerberos', 'internal'], it is unable to connect using kerberos and then >> the user has provided a valid email and password so we will set ' >> *authentication_mode*' to 'internal' and the rest of the logic will >> be based on that flag. >> >> This was already taken care of. > >> >> - >> >> >> - Connect to any database server and check backend logs following >> error is visible: >> - KeyError: 'KRB5CCNAME' *Solution*: It should not call >> "kerberos_validate_ticket()" function until AUTHENTICATION_SOURCES is >> 'kerberos' and Server Mode is true. >> >> Fixed. > > >> *AUTHENTICATION_SOURCES = ['kerberos']:* >> >> - Kerberos is not set up: Open pgAdmin page, enter email and password >> two message box popped up one with valid Kerberos error and the second one >> with "None" as a string. >> >> Fixed > >> >> - Similarly, if AUTHENTICATION_SOURCES = ['kerberos', 'internal'] and >> it is failed to connect using kerberos, then provide an email, and the >> wrong password two message boxes popped up one with Kerberos error and >> another with Password error. >> >> Somehow, I couldn't find the fix for this issue, for now we can ignore > this as this will not affect the login process. > >> >> - In the User Management dialog 'kerberos' should not be visible in >> the authentication source dropdown. As there is no point creating kerberos >> user from there. >> >> We have provided an option to add manual users for Kerberos also the same > as LDAP. > >> >> - Add local server(without kerberos) to the browser tree, set >> "Kerberos Authentication?" to True, try to connect by providing the >> password it always returns "fe_sendauth: no password supplied" error. If >> possible can we identify and change the error message? >> >> Fixed > >> >> - Add database server where kerberos authentication is ON, make >> changes in pg_hba.conf with the wrong user name, then try to connect to the >> database server. The server tries to connect and the spinner is visible and >> never stops. It should raise a proper error message. There are some other >> scenarios where entries in pg_hba.conf is wrong. >> >> Fixed > >> >> - *Suggestion 1*: As per current implementation even if "Kerberos >> Authentication?" is set to false the user can connect to the database >> server by providing any password or blank password. It is difficult for the >> user to identify it is connected using GSSAPI. I would suggest providing >> the control in the properties dialog which tells the database server is >> connected using GSSAPI. >> >> I have removed the old implementation in which the user was able to > connect the PostgresQL even if a user has not selected "Kerberos > Authentication" but we have a valid kerberos ticket and pg_hba is > configured to support it. So, now users can get the idea about the > connection through The "Kerberos authentication" flag displayed on the > properties tab. > >> >> - *Suggestion 2*: If it is possible to detect that the database >> server is connected using Kerberos then we should disable the 'Username' >> control as for Kerberos both the users (pgadmin user and database user ) >> must be the same. >> >> >> *Note:- *pgAdmin on OSX not working with Kerberos authentication. Failed >> with error "Your GSSAPI implementation does not have support for >> manipulating credential stores directly" Need to document this behavior. >> > > Thanks, > khushboo > >> >> *Code review still remains, which I'll be started after the above fixes.* >> >> On Wed, Apr 14, 2021 at 2:06 PM Khushboo Vashi < >> [email protected]> wrote: >> >>> Hi, >>> >>> Please find the attached patch with some minor improvements. >>> >>> Thanks, >>> Khushboo >>> >>> On Wed, Apr 7, 2021 at 11:50 PM Khushboo Vashi < >>> [email protected]> wrote: >>> >>>> Hi, >>>> >>>> Please find the attached patch for RM 6158: Support Kerberos >>>> Authentication - Phase 2. >>>> This patch includes the support for logging into PostgreSQL servers >>>> with Kerberos authentication. >>>> >>>> Thanks, >>>> Khushboo >>>> >>>> >> >> -- >> *Thanks & Regards* >> *Akshay Joshi* >> *pgAdmin Hacker | Principal Software Architect* >> *EDB Postgres <http://edbpostgres.com>* >> >> *Mobile: +91 976-788-8246* >> > -- *Thanks & Regards* *Akshay Joshi* *pgAdmin Hacker | Principal Software Architect* *EDB Postgres <http://edbpostgres.com>* *Mobile: +91 976-788-8246* ^ permalink raw reply [nested|flat] 5+ messages in thread
end of thread, other threads:[~2021-05-03 10:41 UTC | newest] Thread overview: 5+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-04-08 06:50 [pgAdmin4][Patch] - RM 6158 - Logging into PostgreSQL servers with Kerberos Authentication Khushboo Vashi <[email protected]> 2021-04-14 08:35 ` Khushboo Vashi <[email protected]> 2021-04-26 07:12 ` Akshay Joshi <[email protected]> 2021-05-03 09:20 ` Khushboo Vashi <[email protected]> 2021-05-03 10:41 ` Akshay Joshi <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox