From 72f3730c34ed8f741dfee880551c30632dfec263 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Mon, 3 May 2021 16:10:45 +0530 Subject: [PATCH] Added support to connect PostgreSQL servers via Kerberos authentication. Fixes #6158 --- docs/en_US/release_notes_5_3.rst | 1 + web/config.py | 3 + web/migrations/versions/d0bc9f32b2b9_.py | 28 +++++++ web/pgadmin/authenticate/__init__.py | 84 +++++++++++++++++-- web/pgadmin/authenticate/kerberos.py | 24 +++++- .../authenticate/static/js/kerberos.js | 59 +++++++++++++ web/pgadmin/browser/__init__.py | 17 +++- .../browser/server_groups/servers/__init__.py | 27 ++++-- .../servers/databases/__init__.py | 1 + .../servers/databases/static/js/database.js | 53 ++++++++---- .../server_groups/servers/static/js/server.js | 63 ++++++++++---- web/pgadmin/browser/static/js/browser.js | 13 ++- .../browser/templates/browser/js/constants.js | 17 ++++ .../tests/test_kerberos_with_mocking.py | 48 +++++++++-- web/pgadmin/misc/bgprocess/processes.py | 8 +- web/pgadmin/model/__init__.py | 3 +- web/pgadmin/setup/data_directory.py | 19 +++++ web/pgadmin/templates/base.html | 1 + .../backup/static/js/backup_dialog_wrapper.js | 23 ++++- .../tools/debugger/static/js/debugger.js | 20 ++++- .../tools/sqleditor/static/js/sqleditor.js | 23 ++++- web/pgadmin/tools/user_management/__init__.py | 7 +- .../static/js/user_management.js | 22 +++-- .../user_management/js/current_user.js | 3 +- .../utils/driver/psycopg2/connection.py | 26 +++++- .../utils/driver/psycopg2/server_manager.py | 1 + web/pgadmin/utils/master_password.py | 2 +- web/webpack.shim.js | 3 + 28 files changed, 509 insertions(+), 90 deletions(-) create mode 100644 web/migrations/versions/d0bc9f32b2b9_.py create mode 100644 web/pgadmin/authenticate/static/js/kerberos.js create mode 100644 web/pgadmin/browser/templates/browser/js/constants.js diff --git a/docs/en_US/release_notes_5_3.rst b/docs/en_US/release_notes_5_3.rst index 66cf86a52..0b7b1c720 100644 --- a/docs/en_US/release_notes_5_3.rst +++ b/docs/en_US/release_notes_5_3.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #6158 `_ - Added support to connect PostgreSQL servers via Kerberos authentication. Housekeeping ************ 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 = '' 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 + + +@blueprint.route("/kerberos/update_ticket", + endpoint="kerberos_update_ticket", methods=["GET"]) +@pgCSRFProtect.exempt +@login_required +def kerberos_update_ticket(): + """ + Update the kerberos ticket. + """ + from werkzeug.datastructures import Headers + headers = Headers() + + authorization = request.headers.get("Authorization", None) + + if authorization is None: + # Send the Negotiate header to the client + # if Kerberos ticket is not found. + headers.add('WWW-Authenticate', 'Negotiate') + return Response("Unauthorised", 401, headers) + else: + source = get_auth_sources(KERBEROS) + auth_header = authorization.split() + in_token = auth_header[1] + + # Validate the Kerberos ticket + status, context = source.negotiate_start(in_token) + if status: + return Response("Ticket updated successfully.") + + return Response(context, 500) + + +@blueprint.route("/kerberos/validate_ticket", + endpoint="kerberos_validate_ticket", methods=["GET"]) +@pgCSRFProtect.exempt +@login_required +def kerberos_validate_ticket(): + """ + Return the kerberos ticket lifetime left after getting the + ticket from the credential cache + """ + import gssapi + + try: + del_creds = gssapi.Credentials(store={'ccache': session['KRB5CCNAME']}) + creds = del_creds.acquire(store={'ccache': session['KRB5CCNAME']}) + except Exception as e: + current_app.logger.exception(e) + return internal_server_error(errormsg=str(e)) + + return make_json_response( + data={'ticket_lifetime': creds.lifetime}, + status=200 + ) diff --git a/web/pgadmin/authenticate/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(): ) +@blueprint.route("/js/constants.js") +@pgCSRFProtect.exempt +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 = 'user@PGADMIN.ORG' - 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 = 'user@PGADMIN.ORG' + + 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 1ef690e55..e396fdf7d 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; @@ -2454,9 +2455,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