Added support to connect PostgreSQL servers via Kerberos authentication. Fixes #6158

This commit is contained in:
Khushboo Vashi 2021-05-03 16:10:45 +05:30 committed by Akshay Joshi
parent aa9a4c30d3
commit 72f3730c34
28 changed files with 509 additions and 90 deletions

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features New features
************ ************
| `Issue #6158 <https://redmine.postgresql.org/issues/6158>`_ - Added support to connect PostgreSQL servers via Kerberos authentication.
Housekeeping Housekeeping
************ ************

View File

@ -634,6 +634,9 @@ KRB_KTNAME = '<KRB5_KEYTAB_FILE>'
KRB_AUTO_CREATE_USER = True KRB_AUTO_CREATE_USER = True
KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
########################################################################## ##########################################################################
# Local config settings # Local config settings
########################################################################## ##########################################################################

View File

@ -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

View File

@ -13,10 +13,13 @@ import flask
import pickle import pickle
from flask import current_app, flash, Response, request, url_for,\ from flask import current_app, flash, Response, request, url_for,\
render_template 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.views import _security, _ctx
from flask_security.utils import config_value, get_post_logout_redirect, \ from flask_security.utils import config_value, get_post_logout_redirect, \
get_post_login_redirect, logout_user get_post_login_redirect, logout_user
from pgadmin.utils.ajax import make_json_response, internal_server_error
import os
from flask import session from flask import session
@ -34,7 +37,9 @@ class AuthenticateModule(PgAdminModule):
def get_exposed_url_endpoints(self): def get_exposed_url_endpoints(self):
return ['authenticate.login', return ['authenticate.login',
'authenticate.kerberos_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='') blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
@ -55,6 +60,12 @@ def kerberos_login():
@pgCSRFProtect.exempt @pgCSRFProtect.exempt
def kerberos_logout(): def kerberos_logout():
logout_user() 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", return Response(render_template("browser/kerberos_logout.html",
login_url=url_for('security.login'), login_url=url_for('security.login'),
)) ))
@ -165,6 +176,8 @@ class AuthSourceManager():
if self.form.data['email'] and self.form.data['password'] and \ if self.form.data['email'] and self.form.data['password'] and \
source.get_source_name() == KERBEROS: source.get_source_name() == KERBEROS:
msg = gettext('pgAdmin internal user authentication'
' is not enabled, please contact administrator.')
continue continue
status, msg = source.authenticate(self.form) status, msg = source.authenticate(self.form)
@ -173,11 +186,13 @@ class AuthSourceManager():
# OR When kerberos authentication failed while accessing pgadmin, # OR When kerberos authentication failed while accessing pgadmin,
# we need to break the loop as no need to authenticate further # we need to break the loop as no need to authenticate further
# even if the authentication sources set to multiple # even if the authentication sources set to multiple
if not status and (hasattr(msg, 'status') and if not status:
msg.status == '401 UNAUTHORIZED') or \ if (hasattr(msg, 'status') and
(source.get_source_name() == KERBEROS and msg.status == '401 UNAUTHORIZED') or\
request.method == 'GET'): (source.get_source_name() ==
break KERBEROS and
request.method == 'GET'):
break
if status: if status:
self.set_source(source) self.set_source(source)
@ -224,3 +239,58 @@ def init_app(app):
AuthSourceRegistry.load_auth_sources() AuthSourceRegistry.load_auth_sources()
return 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
)

View File

@ -10,7 +10,7 @@
"""A blueprint module implementing the Spnego/Kerberos authentication.""" """A blueprint module implementing the Spnego/Kerberos authentication."""
import base64 import base64
from os import environ from os import environ, path
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from flask_babelex import gettext from flask_babelex import gettext
@ -128,19 +128,37 @@ class KerberosAuthentication(BaseAuthentication):
if out_token and not context.complete: if out_token and not context.complete:
return False, out_token return False, out_token
if context.complete: 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 return True, context
else: else:
return False, None return False, None
def negotiate_end(self, context): def negotiate_end(self, context):
# Free gss_cred_id_t # Free Delegated Credentials
del_creds = getattr(context, 'delegated_creds', None) del_creds = getattr(context, 'delegated_creds', None)
if del_creds: if del_creds:
deleg_creds = context.delegated_creds deleg_creds = context.delegated_creds
del(deleg_creds) del(deleg_creds)
def __auto_create_user(self, username): 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) username = str(username)
if config.KRB_AUTO_CREATE_USER: if config.KRB_AUTO_CREATE_USER:
user = User.query.filter_by( user = User.query.filter_by(

View File

@ -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};

View File

@ -50,7 +50,7 @@ from pgadmin.utils.master_password import validate_master_password, \
set_crypt_key, process_masterpass_disabled set_crypt_key, process_masterpass_disabled
from pgadmin.model import User from pgadmin.model import User
from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\ from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\
INTERNAL, KERBEROS INTERNAL, KERBEROS, LDAP
try: try:
from flask_security.views import default_render_json from flask_security.views import default_render_json
@ -197,7 +197,8 @@ class BrowserModule(PgAdminModule):
for name, script in [ for name, script in [
[PGADMIN_BROWSER, 'js/browser'], [PGADMIN_BROWSER, 'js/browser'],
['pgadmin.browser.endpoints', 'js/endpoints'], ['pgadmin.browser.endpoints', 'js/endpoints'],
['pgadmin.browser.error', 'js/error'] ['pgadmin.browser.error', 'js/error'],
['pgadmin.browser.constants', 'js/constants']
]: ]:
scripts.append({ scripts.append({
'name': name, '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") @blueprint.route("/js/error.js")
@pgCSRFProtect.exempt @pgCSRFProtect.exempt
@login_required @login_required

View File

@ -253,7 +253,8 @@ class ServerModule(sg.ServerGroupPluginModule):
errmsg=errmsg, errmsg=errmsg,
user_id=server.user_id, user_id=server.user_id,
user_name=server.username, user_name=server.username,
shared=server.shared shared=server.shared,
is_kerberos_conn=bool(server.kerberos_conn),
) )
@property @property
@ -547,7 +548,8 @@ class ServerNode(PGChildNodeView):
if server.tunnel_password is not None else False, if server.tunnel_password is not None else False,
errmsg=errmsg, errmsg=errmsg,
user_name=server.username, 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, if server.tunnel_password is not None else False,
errmsg=errmsg, errmsg=errmsg,
shared=server.shared, 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_username': 'tunnel_username',
'tunnel_authentication': 'tunnel_authentication', 'tunnel_authentication': 'tunnel_authentication',
'tunnel_identity_file': 'tunnel_identity_file', 'tunnel_identity_file': 'tunnel_identity_file',
'shared': 'shared' 'shared': 'shared',
'kerberos_conn': 'kerberos_conn',
} }
disp_lbl = { disp_lbl = {
@ -985,7 +989,8 @@ class ServerNode(PGChildNodeView):
'tunnel_username': tunnel_username, 'tunnel_username': tunnel_username,
'tunnel_identity_file': server.tunnel_identity_file 'tunnel_identity_file': server.tunnel_identity_file
if server.tunnel_identity_file else None, 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) return ajax_response(response)
@ -1072,7 +1077,8 @@ class ServerNode(PGChildNodeView):
tunnel_authentication=data.get('tunnel_authentication', 0), tunnel_authentication=data.get('tunnel_authentication', 0),
tunnel_identity_file=data.get('tunnel_identity_file', None), tunnel_identity_file=data.get('tunnel_identity_file', None),
shared=data.get('shared', 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.add(server)
db.session.commit() db.session.commit()
@ -1154,7 +1160,8 @@ class ServerNode(PGChildNodeView):
else 'pg', else 'pg',
version=manager.version version=manager.version
if manager and 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: except Exception as e:
current_app.logger.exception(e) current_app.logger.exception(e)
return internal_server_error(errormsg=str(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) conn_passwd = getattr(conn, 'password', None)
if conn_passwd is None and not server.save_password and \ if conn_passwd is None and not server.save_password and \
server.passfile is None and server.service is None: 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}" "Could not connect to server(#{0}) - '{1}'.\nError: {2}"
.format(server.id, server.name, errmsg) .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, return self.get_response_for_password(server, 401, True,
True, errmsg) True, errmsg)
else: else:
@ -1467,6 +1477,7 @@ class ServerNode(PGChildNodeView):
'is_password_saved': bool(server.save_password), 'is_password_saved': bool(server.save_password),
'is_tunnel_password_saved': True 'is_tunnel_password_saved': True
if server.tunnel_password is not None else False, if server.tunnel_password is not None else False,
'is_kerberos_conn': bool(server.kerberos_conn),
} }
) )

View File

@ -490,6 +490,7 @@ class DatabaseView(PGChildNodeView):
did, errmsg did, errmsg
) )
) )
return internal_server_error(errmsg) return internal_server_error(errmsg)
else: else:
current_app.logger.info( current_app.logger.info(

View File

@ -10,9 +10,10 @@
define('pgadmin.node.database', [ define('pgadmin.node.database', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/utils', 'sources/pgadmin', 'pgadmin.browser.utils', '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', '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']) { if (!pgBrowser.Nodes['coll-database']) {
pgBrowser.Nodes['coll-database'] = pgBrowser.Nodes['coll-database'] =
@ -556,24 +557,39 @@ define('pgadmin.node.database', [
onFailure = function( onFailure = function(
xhr, status, error, _model, _data, _tree, _item, _status xhr, status, error, _model, _data, _tree, _item, _status
) { ) {
if (!_status) { if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) {
tree.setInode(_item); tree.addIcon(_item, {icon: 'icon-server-connecting'});
tree.addIcon(_item, {icon: 'icon-database-not-connected'}); let fetchTicket = Kerberos.fetch_ticket();
} fetchTicket.then(
function() {
Alertify.pgNotifier('error', xhr, error, function(msg) {
setTimeout(function() {
if (msg == 'CRYPTKEY_SET') {
connect_to_database(_model, _data, _tree, _item, _wasConnected); connect_to_database(_model, _data, _tree, _item, _wasConnected);
} else { },
Alertify.dlgServerPass( function(error) {
gettext('Connect to database'), tree.setInode(_item);
msg, _model, _data, _tree, _item, _status, tree.addIcon(_item, {icon: 'icon-database-not-connected'});
onSuccess, onFailure, onCancel Alertify.pgNotifier(error, xhr, gettext('Connect to database.'));
).resizeTo();
} }
}, 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( onSuccess = function(
res, model, _data, _tree, _item, _connected res, model, _data, _tree, _item, _connected
@ -640,6 +656,7 @@ define('pgadmin.node.database', [
if (xhr.status === 410) { if (xhr.status === 410) {
error = gettext('Error: Object not found - %s.', error); error = gettext('Error: Object not found - %s.', error);
} }
return onFailure( return onFailure(
xhr, status, error, obj, data, tree, item, wasConnected xhr, status, error, obj, data, tree, item, wasConnected
); );

View File

@ -13,11 +13,13 @@ define('pgadmin.node.server', [
'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user', 'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.alertifyjs', 'pgadmin.backform',
'sources/browser/server_groups/servers/model_validation', 'sources/browser/server_groups/servers/model_validation',
'pgadmin.authenticate.kerberos',
'pgadmin.browser.constants',
'pgadmin.browser.server.privilege', 'pgadmin.browser.server.privilege',
], function( ], function(
gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser,
supported_servers, current_user, Alertify, Backform, supported_servers, current_user, Alertify, Backform,
modelValidation modelValidation, Kerberos, pgConst,
) { ) {
if (!pgBrowser.Nodes['server']) { 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, 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) { visible: function(model) {
return model.get('connect_now') && model.isNew(); 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?'), id: 'save_password', controlLabel: gettext('Save password?'),
type: 'checkbox', group: gettext('Connection'), mode: ['create'], 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(); return model.get('connect_now') && model.isNew();
}, },
disabled: function() { disabled: function(model) {
if (!current_user.allow_save_password) if (!current_user.allow_save_password || model.get('kerberos_conn'))
return true; return true;
return false; return false;
@ -1279,19 +1297,32 @@ define('pgadmin.node.server', [
} }
} }
if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) {
Alertify.pgNotifier('error', xhr, error, function(msg) { tree.addIcon(_item, {icon: 'icon-server-connecting'});
setTimeout(function() { let fetchTicket = Kerberos.fetch_ticket();
if (msg == 'CRYPTKEY_SET') { fetchTicket.then(
function() {
connect_to_server(_node, _data, _tree, _item, _wasConnected); connect_to_server(_node, _data, _tree, _item, _wasConnected);
} else { },
Alertify.dlgServerPass( function() {
gettext('Connect to Server'), tree.addIcon(_item, {icon: 'icon-server-not-connected'});
msg, _node, _data, _tree, _item, _wasConnected Alertify.pgNotifier('Connection error', xhr, gettext('Connect to server.'));
).resizeTo();
} }
}, 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) { onSuccess = function(res, node, _data, _tree, _item, _wasConnected) {
if (res && res.data) { if (res && res.data) {

View File

@ -12,19 +12,22 @@ define('pgadmin.browser', [
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', './toolbar', 'pgadmin.help', 'sources/check_node_visibility', './toolbar', 'pgadmin.help',
'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.browser.utils', 'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.authenticate.kerberos',
'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin',
'jquery.acitree',
'pgadmin.browser.preferences', 'pgadmin.browser.messages', 'pgadmin.browser.preferences', 'pgadmin.browser.messages',
'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout',
'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame',
'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', '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( ], function(
tree, tree,
gettext, url_for, require, $, _, gettext, url_for, require, $, _,
Bootstrap, pgAdmin, Alertify, codemirror, Bootstrap, pgAdmin, Alertify, codemirror,
checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow,
Kerberos
) { ) {
window.jQuery = window.$ = $; window.jQuery = window.$ = $;
// Some scripts do export their object in the window only. // 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); csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
Kerberos.validate_kerberos_ticket();
var panelEvents = {}; var panelEvents = {};
panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() { panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() {
if (this.isVisible()) { if (this.isVisible()) {

View File

@ -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 }}'
}
});

View File

@ -12,6 +12,7 @@ from pgadmin.utils.route import BaseTestGenerator
from regression.python_test_utils import test_utils as utils from regression.python_test_utils import test_utils as utils
from pgadmin.authenticate.registry import AuthSourceRegistry from pgadmin.authenticate.registry import AuthSourceRegistry
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from werkzeug.datastructures import Headers
class KerberosLoginMockTestCase(BaseTestGenerator): class KerberosLoginMockTestCase(BaseTestGenerator):
@ -30,6 +31,11 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
auth_source=['kerberos'], auth_source=['kerberos'],
auto_create_user=True, auto_create_user=True,
flag=2 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( self.skipTest(
"Can not run Kerberos Authentication in the Desktop mode." "Can not run Kerberos Authentication in the Desktop mode."
) )
self.test_authorized() 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): def test_unauthorized(self):
""" """
@ -73,13 +84,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
passed on to the routed method. passed on to the routed method.
""" """
class delCrads: del_crads = self.mock_negotiate_start()
def __init__(self):
self.initiator_name = 'user@PGADMIN.ORG'
del_crads = delCrads()
AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock(
return_value=[True, del_crads])
res = self.tester.login(None, res = self.tester.login(None,
None, None,
True, True,
@ -89,6 +94,33 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
respdata = 'Gravatar image for %s' % del_crads.initiator_name respdata = 'Gravatar image for %s' % del_crads.initiator_name
self.assertTrue(respdata in res.data.decode('utf8')) 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): def tearDown(self):
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'

View File

@ -24,10 +24,11 @@ import logging
from pgadmin.utils import u_encode, file_quote, fs_encoding, \ from pgadmin.utils import u_encode, file_quote, fs_encoding, \
get_complete_file_path, get_storage_directory, IS_WIN get_complete_file_path, get_storage_directory, IS_WIN
from pgadmin.browser.server_groups.servers.utils import does_server_exists from pgadmin.browser.server_groups.servers.utils import does_server_exists
from pgadmin.utils.constants import KERBEROS
import pytz import pytz
from dateutil import parser from dateutil import parser
from flask import current_app from flask import current_app, session
from flask_babelex import gettext as _ from flask_babelex import gettext as _
from flask_security import current_user from flask_security import current_user
@ -278,13 +279,16 @@ class BatchProcess(object):
env['PROCID'] = self.id env['PROCID'] = self.id
env['OUTDIR'] = self.log_dir env['OUTDIR'] = self.log_dir
env['PGA_BGP_FOREGROUND'] = "1" 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: if self.env:
env.update(self.env) env.update(self.env)
if cb is not None: if cb is not None:
cb(env) cb(env)
if os.name == 'nt': if os.name == 'nt':
DETACHED_PROCESS = 0x00000008 DETACHED_PROCESS = 0x00000008
from subprocess import CREATE_NEW_PROCESS_GROUP from subprocess import CREATE_NEW_PROCESS_GROUP

View File

@ -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_identity_file = db.Column(db.String(64), nullable=True)
tunnel_password = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(db.String(64), nullable=True)
shared = db.Column(db.Boolean(), nullable=False) shared = db.Column(db.Boolean(), nullable=False)
kerberos_conn = db.Column(db.Boolean(), nullable=False)
@property @property
def serialize(self): def serialize(self):

View File

@ -9,6 +9,8 @@
import os import os
import getpass import getpass
from flask import current_app
from pgadmin.utils.constants import KERBEROS
FAILED_CREATE_DIR = \ FAILED_CREATE_DIR = \
"ERROR : Failed to create the directory {}:\n {}" "ERROR : Failed to create the directory {}:\n {}"
@ -104,3 +106,20 @@ def create_app_data_directory(config):
getpass.getuser(), getpass.getuser(),
config.APP_VERSION)) config.APP_VERSION))
exit(1) 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)

View File

@ -48,6 +48,7 @@
'pgadmin.browser.utils': "{{ url_for('browser.index') }}" + "js/utils", 'pgadmin.browser.utils': "{{ url_for('browser.index') }}" + "js/utils",
'pgadmin.browser.endpoints': "{{ url_for('browser.index') }}" + "js/endpoints", 'pgadmin.browser.endpoints': "{{ url_for('browser.index') }}" + "js/endpoints",
'pgadmin.browser.messages': "{{ url_for('browser.index') }}" + "js/messages", '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.server.supported_servers': "{{ url_for('browser.index') }}" + "server/supported_servers",
'pgadmin.user_management.current_user': "{{ url_for('user_management.index') }}" + "current_user", 'pgadmin.user_management.current_user': "{{ url_for('user_management.index') }}" + "current_user",
'translations': "{{ url_for('tools.index') }}" + "translations" 'translations': "{{ url_for('tools.index') }}" + "translations"

View File

@ -13,6 +13,8 @@ import gettext from '../../../../static/js/gettext';
import url_for from '../../../../static/js/url_for'; import url_for from '../../../../static/js/url_for';
import _ from 'underscore'; import _ from 'underscore';
import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper'; 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 { export class BackupDialogWrapper extends DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog, constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
@ -165,10 +167,29 @@ export class BackupDialogWrapper extends DialogWrapper {
); );
this.setExtraParameters(selectedTreeNode, treeInfo); 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( axios.post(
baseUrl, baseUrl,
this.view.model.toJSON() backupDate
).then(function (res) { ).then(function (res) {
if (res.data.success) { if (res.data.success) {
dialog.alertify.success(gettext('Backup job created.'), 5); dialog.alertify.success(gettext('Backup job created.'), 5);

View File

@ -13,11 +13,11 @@ define([
'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform', 'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform',
'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils', 'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils',
'tools/datagrid/static/js/show_query_tool', 'sources/utils', 'tools/datagrid/static/js/show_query_tool', 'sources/utils',
'wcdocker', 'pgadmin.browser.frame', 'pgadmin.authenticate.kerberos', 'wcdocker', 'pgadmin.browser.frame',
], function( ], function(
gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
CodeMirror, Backform, get_function_arguments, debuggerUtils, showQueryTool, CodeMirror, Backform, get_function_arguments, debuggerUtils, showQueryTool,
pgadminUtils, pgadminUtils, Kerberos
) { ) {
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {}, var pgTools = pgAdmin.Tools = pgAdmin.Tools || {},
wcDocker = window.wcDocker; wcDocker = window.wcDocker;
@ -472,8 +472,20 @@ define([
.fail(function(xhr) { .fail(function(xhr) {
try { try {
var err = JSON.parse(xhr.responseText); var err = JSON.parse(xhr.responseText);
if (err.success == 0) { if (err.errormsg.search('Ticket expired') !== -1) {
Alertify.alert(gettext('Debugger Error'), err.errormsg); 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) { } catch (e) {
console.warn(e.stack || e); console.warn(e.stack || e);

View File

@ -51,6 +51,7 @@ define('tools.querytool', [
'sources/window', 'sources/window',
'sources/is_native', 'sources/is_native',
'sources/sqleditor/macro', 'sources/sqleditor/macro',
'pgadmin.authenticate.kerberos',
'sources/../bundle/slickgrid', 'sources/../bundle/slickgrid',
'pgadmin.file_manager', 'pgadmin.file_manager',
'slick.pgadmin.formatters', 'slick.pgadmin.formatters',
@ -65,7 +66,7 @@ define('tools.querytool', [
GeometryViewer, historyColl, queryHist, querySources, GeometryViewer, historyColl, queryHist, querySources,
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
pgWindow, isNative, MacroHandler) { pgWindow, isNative, MacroHandler, Kerberos) {
/* Return back, this has been called more than once */ /* Return back, this has been called more than once */
if (pgAdmin.SqlEditor) if (pgAdmin.SqlEditor)
return 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)); pgBrowser.report_error(gettext('Error fetching rows - %s.', xhr.statusText), xhr.responseJSON.errormsg, undefined, self.close.bind(self));
} }
} else { } else {
pgBrowser.Events.trigger( if (xhr.responseText.search('Ticket expired') !== -1) {
'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error 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
);
}
} }
}); });
}, },

View File

@ -25,7 +25,7 @@ from pgadmin.utils.ajax import make_response as ajax_response, \
make_json_response, bad_request, internal_server_error, forbidden make_json_response, bad_request, internal_server_error, forbidden
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\ 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.utils.validation_utils import validate_email
from pgadmin.model import db, Role, User, UserPreference, Server, \ from pgadmin.model import db, Role, User, UserPreference, Server, \
ServerGroup, Process, Setting, roles_users, SharedServer ServerGroup, Process, Setting, roles_users, SharedServer
@ -157,7 +157,6 @@ def script():
@pgCSRFProtect.exempt @pgCSRFProtect.exempt
@login_required @login_required
def current_user_info(): def current_user_info():
return Response( return Response(
response=render_template( response=render_template(
"user_management/js/current_user.js", "user_management/js/current_user.js",
@ -176,7 +175,9 @@ def current_user_info():
allow_save_tunnel_password='true' if allow_save_tunnel_password='true' if
config.ALLOW_SAVE_TUNNEL_PASSWORD and session[ config.ALLOW_SAVE_TUNNEL_PASSWORD and session[
'allow_save_password'] else 'false', '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, status=200,
mimetype=MIMETYPE_APP_JS mimetype=MIMETYPE_APP_JS

View File

@ -10,11 +10,11 @@
define([ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs', 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs',
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node', 'pgadmin.backform', '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', 'backgrid.select.all', 'backgrid.filter',
], function( ], function(
gettext, url_for, $, _, alertify, pgBrowser, Backbone, Backgrid, Backform, 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. // if module is already initialized, refer to that.
@ -25,7 +25,9 @@ define([
var USERURL = url_for('user_management.users'), var USERURL = url_for('user_management.users'),
ROLEURL = url_for('user_management.roles'), ROLEURL = url_for('user_management.roles'),
SOURCEURL = url_for('user_management.auth_sources'), 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, AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false,
userFilter = function(collection) { userFilter = function(collection) {
return (new Backgrid.Extension.ClientSideFilter({ return (new Backgrid.Extension.ClientSideFilter({
@ -589,7 +591,17 @@ define([
} }
} else { } else {
if (!!this.get('username') && this.collection.nonFilter.where({ 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) { }).length > 1) {
errmsg = gettext('The username %s already exists.', errmsg = gettext('The username %s already exists.',
this.get('username') this.get('username')
@ -1041,7 +1053,7 @@ define([
saveUser: function(m) { saveUser: function(m) {
var d = m.toJSON(true); 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.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.get('newPassword') || !m.get('confirmPassword') || m.get('newPassword') != m.get('confirmPassword')))
|| (!m.isNew() && m.get('newPassword') != m.get('confirmPassword'))) { || (!m.isNew() && m.get('newPassword') != m.get('confirmPassword'))) {

View File

@ -15,6 +15,7 @@ define('pgadmin.user_management.current_user', [], function() {
'name': '{{ name }}', 'name': '{{ name }}',
'allow_save_password': {{ allow_save_password }}, 'allow_save_password': {{ allow_save_password }},
'allow_save_tunnel_password': {{ allow_save_tunnel_password }}, 'allow_save_tunnel_password': {{ allow_save_tunnel_password }},
'auth_sources': {{ auth_sources }} 'auth_sources': {{ auth_sources }},
'current_auth_source': '{{ current_auth_source }}'
} }
}); });

View File

@ -18,11 +18,13 @@ import select
import datetime import datetime
from collections import deque from collections import deque
import psycopg2 import psycopg2
from flask import g, current_app import threading
from flask import g, current_app, session
from flask_babelex import gettext from flask_babelex import gettext
from flask_security import current_user from flask_security import current_user
from pgadmin.utils.crypto import decrypt, encrypt from pgadmin.utils.crypto import decrypt, encrypt
from psycopg2.extensions import encodings from psycopg2.extensions import encodings
from os import environ
import config import config
from pgadmin.model import User 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 import csv
from pgadmin.utils.master_password import get_crypt_key from pgadmin.utils.master_password import get_crypt_key
from io import StringIO from io import StringIO
from pgadmin.utils.constants import KERBEROS
lock = threading.Lock()
_ = gettext _ = gettext
@ -313,6 +318,13 @@ class Connection(BaseConnection):
os.environ['PGAPPNAME'] = '{0} - {1}'.format( os.environ['PGAPPNAME'] = '{0} - {1}'.format(
config.APP_NAME, conn_id) 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( pg_conn = psycopg2.connect(
host=manager.local_bind_host if manager.use_ssh_tunnel host=manager.local_bind_host if manager.use_ssh_tunnel
else manager.host, else manager.host,
@ -340,7 +352,13 @@ class Connection(BaseConnection):
if self.async_ == 1: if self.async_ == 1:
self._wait(pg_conn) self._wait(pg_conn)
if config.SERVER_MODE and \
session['_auth_source_manager_obj']['current_source'] == \
KERBEROS:
environ['KRB5CCNAME'] = ''
except psycopg2.Error as e: except psycopg2.Error as e:
environ['KRB5CCNAME'] = ''
manager.stop_ssh_tunnel() manager.stop_ssh_tunnel()
if e.pgerror: if e.pgerror:
msg = e.pgerror msg = e.pgerror
@ -358,6 +376,11 @@ class Connection(BaseConnection):
) )
) )
return False, msg 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 # Overwrite connection notice attr to support
# more than 50 notices at a time # more than 50 notices at a time
@ -1438,7 +1461,6 @@ Failed to reset the connection to the server due to following error:
Args: Args:
conn: connection object conn: connection object
""" """
while True: while True:
state = conn.poll() state = conn.poll()
if state == psycopg2.extensions.POLL_OK: if state == psycopg2.extensions.POLL_OK:

View File

@ -105,6 +105,7 @@ class ServerManager(object):
self.tunnel_identity_file = None self.tunnel_identity_file = None
self.tunnel_password = None self.tunnel_password = None
self.kerberos_conn = server.kerberos_conn
for con in self.connections: for con in self.connections:
self.connections[con]._release() self.connections[con]._release()

View File

@ -34,7 +34,7 @@ def get_crypt_key():
and not config.SERVER_MODE and enc_key is None: and not config.SERVER_MODE and enc_key is None:
return False, None return False, None
elif config.SERVER_MODE and \ elif config.SERVER_MODE and \
session['_auth_source_manager_obj']['source_friendly_name']\ session['_auth_source_manager_obj']['current_source']\
== KERBEROS: == KERBEROS:
return True, session['kerberos_key'] if 'kerberos_key' in session \ return True, session['kerberos_key'] if 'kerberos_key' in session \
else None else None

View File

@ -174,11 +174,13 @@ var webpackShimConfig = {
'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'), 'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'),
'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'), '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': path.join(__dirname, './pgadmin/browser/static/js/browser'),
'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'), '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.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),
'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'), 'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'),
'pgadmin.browser.endpoints': '/browser/js/endpoints', '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.error': path.join(__dirname, './pgadmin/browser/static/js/error'),
'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'), 'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'),
'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'), 'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'),
@ -300,6 +302,7 @@ var webpackShimConfig = {
'pgadmin.browser.messages', 'pgadmin.browser.messages',
'pgadmin.browser.utils', 'pgadmin.browser.utils',
'pgadmin.server.supported_servers', 'pgadmin.server.supported_servers',
'pgadmin.browser.constants',
], ],
// Define list of pgAdmin common libraries to bundle them separately // Define list of pgAdmin common libraries to bundle them separately
// into commons JS from app.bundle.js // into commons JS from app.bundle.js