1) Added support for Kerberos authentication, using SPNEGO to forward the Kerberos tickets through a browser. Fixes #5457

2) Fixed incorrect log information for AUTHENTICATION_SOURCES. Fixes #5829
This commit is contained in:
Khushboo Vashi 2021-01-14 13:46:48 +05:30 committed by Akshay Joshi
parent 9a47e574e3
commit 6ead597b43
22 changed files with 474 additions and 40 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 #5457 <https://redmine.postgresql.org/issues/5457>`_ - Added support for Kerberos authentication, using SPNEGO to forward the Kerberos tickets through a browser.
Housekeeping Housekeeping
************ ************
@ -23,6 +24,7 @@ Bug fixes
| `Issue #5282 <https://redmine.postgresql.org/issues/5282>`_ - Added 'Count Rows' option to the partition sub tables. | `Issue #5282 <https://redmine.postgresql.org/issues/5282>`_ - Added 'Count Rows' option to the partition sub tables.
| `Issue #5488 <https://redmine.postgresql.org/issues/5488>`_ - Improve the explain plan details by showing popup instead of tooltip on clicking of the specified node. | `Issue #5488 <https://redmine.postgresql.org/issues/5488>`_ - Improve the explain plan details by showing popup instead of tooltip on clicking of the specified node.
| `Issue #5571 <https://redmine.postgresql.org/issues/5571>`_ - Added support for expression in exclusion constraints. | `Issue #5571 <https://redmine.postgresql.org/issues/5571>`_ - Added support for expression in exclusion constraints.
| `Issue #5829 <https://redmine.postgresql.org/issues/5829>`_ - Fixed incorrect log information for AUTHENTICATION_SOURCES.
| `Issue #5875 <https://redmine.postgresql.org/issues/5875>`_ - Ensure that the 'template1' database should not be visible after pg_upgrade. | `Issue #5875 <https://redmine.postgresql.org/issues/5875>`_ - Ensure that the 'template1' database should not be visible after pg_upgrade.
| `Issue #5965 <https://redmine.postgresql.org/issues/5965>`_ - Ensure that the macro query result should be download properly. | `Issue #5965 <https://redmine.postgresql.org/issues/5965>`_ - Ensure that the macro query result should be download properly.
| `Issue #5973 <https://redmine.postgresql.org/issues/5973>`_ - Added appropriate help message and a placeholder for letting users know about the account password expiry for Login/Group Role. | `Issue #5973 <https://redmine.postgresql.org/issues/5973>`_ - Added appropriate help message and a placeholder for letting users know about the account password expiry for Login/Group Role.

View File

@ -43,3 +43,4 @@ cryptography<=3.0;
sshtunnel>=0.1.5 sshtunnel>=0.1.5
ldap3>=2.5.1 ldap3>=2.5.1
Flask-BabelEx>=0.9.4 Flask-BabelEx>=0.9.4
gssapi>=1.6.11

View File

@ -535,7 +535,7 @@ ENHANCED_COOKIE_PROTECTION = True
########################################################################## ##########################################################################
# Default setting is internal # Default setting is internal
# External Supported Sources: ldap # External Supported Sources: ldap, kerberos
# Multiple authentication can be achieved by setting this parameter to # Multiple authentication can be achieved by setting this parameter to
# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first, # ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first,
# in case of failure internal authentication will be done. # in case of failure internal authentication will be done.
@ -618,6 +618,26 @@ LDAP_CA_CERT_FILE = ''
LDAP_CERT_FILE = '' LDAP_CERT_FILE = ''
LDAP_KEY_FILE = '' LDAP_KEY_FILE = ''
##########################################################################
# Kerberos Configuration
##########################################################################
KRB_APP_HOST_NAME = DEFAULT_SERVER
# If the default_keytab_name is not set in krb5.conf or
# the KRB_KTNAME environment variable is not set then, explicitly set
# the Keytab file
KRB_KTNAME = '<KRB5_KEYTAB_FILE>'
# After kerberos authentication, user will be added into the SQLite database
# automatically, if set to True.
# Set it to False, if user should not be added automatically,
# in this case Admin has to add the user manually in the SQLite database.
KRB_AUTO_CREATE_USER = True
########################################################################## ##########################################################################
# Local config settings # Local config settings
########################################################################## ##########################################################################

View File

@ -35,6 +35,9 @@ else:
import config import config
from pgadmin import create_app from pgadmin import create_app
from pgadmin.utils import u_encode, fs_encoding, file_quote from pgadmin.utils import u_encode, fs_encoding, file_quote
from pgadmin.utils.constants import INTERNAL, LDAP,\
KERBEROS, SUPPORTED_AUTH_SOURCES
# Get the config database schema version. We store this in pgadmin.model # Get the config database schema version. We store this in pgadmin.model
# as it turns out that putting it in the config files isn't a great idea # as it turns out that putting it in the config files isn't a great idea
from pgadmin.model import SCHEMA_VERSION from pgadmin.model import SCHEMA_VERSION
@ -96,15 +99,11 @@ if config.SERVER_MODE:
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
# Authentication sources # Authentication sources
app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
app.PGADMIN_SUPPORTED_AUTH_SOURCE = ['internal', 'ldap']
if len(config.AUTHENTICATION_SOURCES) > 0: if len(config.AUTHENTICATION_SOURCES) > 0:
app.PGADMIN_EXTERNAL_AUTH_SOURCE = config.AUTHENTICATION_SOURCES[0] app.PGADMIN_EXTERNAL_AUTH_SOURCE = config.AUTHENTICATION_SOURCES[0]
else: else:
app.PGADMIN_EXTERNAL_AUTH_SOURCE = app.PGADMIN_DEFAULT_AUTH_SOURCE app.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
app.logger.debug(
"Authentication Source: %s" % app.PGADMIN_DEFAULT_AUTH_SOURCE)
# Start the web server. The port number should have already been set by the # Start the web server. The port number should have already been set by the
# runtime if we're running in desktop mode, otherwise we'll just use the # runtime if we're running in desktop mode, otherwise we'll just use the

View File

@ -43,6 +43,7 @@ from pgadmin.utils.ajax import internal_server_error, make_json_response
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin import authenticate from pgadmin import authenticate
from pgadmin.utils.security_headers import SecurityHeaders from pgadmin.utils.security_headers import SecurityHeaders
from pgadmin.utils.constants import KERBEROS
# Explicitly set the mime-types so that a corrupted windows registry will not # Explicitly set the mime-types so that a corrupted windows registry will not
# affect pgAdmin 4 to be load properly. This will avoid the issues that may # affect pgAdmin 4 to be load properly. This will avoid the issues that may
@ -695,11 +696,19 @@ def create_app(app_name=None):
) )
abort(401) abort(401)
login_user(user) login_user(user)
elif config.SERVER_MODE and\
app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\
KERBEROS and \
not current_user.is_authenticated and \
request.endpoint in ('redirects.index', 'security.login'):
return authenticate.login()
# if the server is restarted the in memory key will be lost # if the server is restarted the in memory key will be lost
# but the user session may still be active. Logout the user # but the user session may still be active. Logout the user
# to get the key again when login # to get the key again when login
if config.SERVER_MODE and current_user.is_authenticated and \ if config.SERVER_MODE and current_user.is_authenticated and \
app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
KERBEROS and \
current_app.keyManager.get() is None and \ current_app.keyManager.get() is None and \
request.endpoint not in ('security.login', 'security.logout'): request.endpoint not in ('security.login', 'security.logout'):
logout_user() logout_user()

View File

@ -11,16 +11,21 @@
import flask import flask
import pickle import pickle
from flask import current_app, flash from flask import current_app, flash, Response, request, url_for,\
render_template
from flask_babelex import gettext from flask_babelex import gettext
from flask_security import current_user from flask_security import current_user
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 get_post_login_redirect, logout_user
from flask import session from flask import session
import config import config
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils.constants import KERBEROS
from pgadmin.utils.csrf import pgCSRFProtect
from .registry import AuthSourceRegistry from .registry import AuthSourceRegistry
MODULE_NAME = 'authenticate' MODULE_NAME = 'authenticate'
@ -28,12 +33,34 @@ MODULE_NAME = 'authenticate'
class AuthenticateModule(PgAdminModule): 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_logout']
blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='') blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
@blueprint.route("/login/kerberos",
endpoint="kerberos_login", methods=["GET"])
@pgCSRFProtect.exempt
def kerberos_login():
logout_user()
return Response(render_template("browser/kerberos_login.html",
login_url=url_for('security.login'),
))
@blueprint.route("/logout/kerberos",
endpoint="kerberos_logout", methods=["GET"])
@pgCSRFProtect.exempt
def kerberos_logout():
logout_user()
return Response(render_template("browser/kerberos_logout.html",
login_url=url_for('security.login'),
))
@blueprint.route('/login', endpoint='login', methods=['GET', 'POST']) @blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
def login(): def login():
""" """
@ -56,15 +83,24 @@ def login():
if status: if status:
# Login the user # Login the user
status, msg = auth_obj.login() status, msg = auth_obj.login()
current_auth_obj = auth_obj.as_dict()
if not status: if not status:
if current_auth_obj['current_source'] ==\
KERBEROS:
return flask.redirect('{0}?next={1}'.format(url_for(
'authenticate.kerberos_login'), url_for('browser.index')))
flash(gettext(msg), 'danger') flash(gettext(msg), 'danger')
return flask.redirect(get_post_logout_redirect()) return flask.redirect(get_post_logout_redirect())
session['_auth_source_manager_obj'] = auth_obj.as_dict() session['_auth_source_manager_obj'] = current_auth_obj
return flask.redirect(get_post_login_redirect()) return flask.redirect(get_post_login_redirect())
elif isinstance(msg, Response):
return msg
flash(gettext(msg), 'danger') flash(gettext(msg), 'danger')
return flask.redirect(get_post_logout_redirect()) response = flask.redirect(get_post_logout_redirect())
return response
class AuthSourceManager(): class AuthSourceManager():
@ -75,6 +111,7 @@ class AuthSourceManager():
self.auth_sources = sources self.auth_sources = sources
self.source = None self.source = None
self.source_friendly_name = None self.source_friendly_name = None
self.current_source = None
def as_dict(self): def as_dict(self):
""" """
@ -84,9 +121,17 @@ class AuthSourceManager():
res = dict() res = dict()
res['source_friendly_name'] = self.source_friendly_name res['source_friendly_name'] = self.source_friendly_name
res['auth_sources'] = self.auth_sources res['auth_sources'] = self.auth_sources
res['current_source'] = self.current_source
return res return res
def set_current_source(self, source):
self.current_source = source
@property
def get_current_source(self, source):
return self.current_source
def set_source(self, source): def set_source(self, source):
self.source = source self.source = source
@ -115,9 +160,33 @@ class AuthSourceManager():
msg = None msg = None
for src in self.auth_sources: for src in self.auth_sources:
source = get_auth_sources(src) source = get_auth_sources(src)
current_app.logger.debug(
"Authentication initiated via source: %s" %
source.get_source_name())
if self.form.data['email'] and self.form.data['password'] and \
source.get_source_name() == KERBEROS:
continue
status, msg = source.authenticate(self.form) status, msg = source.authenticate(self.form)
# When server sends Unauthorized header to get the ticket over HTTP
# OR When kerberos authentication failed while accessing pgadmin,
# we need to break the loop as no need to authenticate further
# even if the authentication sources set to multiple
if not status:
if (hasattr(msg, 'status') and
msg.status == '401 UNAUTHORIZED') or\
(source.get_source_name() ==
KERBEROS and
request.method == 'GET'):
break
if status: if status:
self.set_source(source) self.set_source(source)
self.set_current_source(source.get_source_name())
if msg is not None and 'username' in msg:
self.form._fields['email'].data = msg['username']
return status, msg return status, msg
return status, msg return status, msg
@ -125,6 +194,9 @@ class AuthSourceManager():
status, msg = self.source.login(self.form) status, msg = self.source.login(self.form)
if status: if status:
self.set_source_friendly_name(self.source.get_friendly_name()) self.set_source_friendly_name(self.source.get_friendly_name())
current_app.logger.debug(
"Authentication and Login successfully done via source : %s" %
self.source.get_source_name())
return status, msg return status, msg

View File

@ -18,6 +18,7 @@ from flask_babelex import gettext
from .registry import AuthSourceRegistry from .registry import AuthSourceRegistry
from pgadmin.model import User from pgadmin.model import User
from pgadmin.utils.validation_utils import validate_email from pgadmin.utils.validation_utils import validate_email
from pgadmin.utils.constants import INTERNAL
@six.add_metaclass(AuthSourceRegistry) @six.add_metaclass(AuthSourceRegistry)
@ -31,7 +32,11 @@ class BaseAuthentication(object):
'INVALID_EMAIL': gettext('Email/Username is not valid') 'INVALID_EMAIL': gettext('Email/Username is not valid')
} }
@abstractproperty @abstractmethod
def get_source_name(self):
pass
@abstractmethod
def get_friendly_name(self): def get_friendly_name(self):
pass pass
@ -82,6 +87,9 @@ class BaseAuthentication(object):
class InternalAuthentication(BaseAuthentication): class InternalAuthentication(BaseAuthentication):
def get_source_name(self):
return INTERNAL
def get_friendly_name(self): def get_friendly_name(self):
return gettext("internal") return gettext("internal")

View File

@ -0,0 +1,138 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""A blueprint module implementing the Spnego/Kerberos authentication."""
import base64
import gssapi
from os import environ
from werkzeug.datastructures import Headers
from flask_babelex import gettext
from flask import Flask, request, Response, session,\
current_app, render_template, flash
import config
from pgadmin.model import User, ServerGroup, db, Role
from pgadmin.tools.user_management import create_user
from pgadmin.utils.constants import KERBEROS
from flask_security.views import _security, _commit, _ctx
from werkzeug.datastructures import MultiDict
from .internal import BaseAuthentication
# Set the Kerberos config file
if config.KRB_KTNAME and config.KRB_KTNAME != '<KRB5_KEYTAB_FILE>':
environ['KRB5_KTNAME'] = config.KRB_KTNAME
class KerberosAuthentication(BaseAuthentication):
def get_source_name(self):
return KERBEROS
def get_friendly_name(self):
return gettext("kerberos")
def validate(self, form):
return True
def authenticate(self, frm):
retval = [True, None]
negotiate = False
headers = Headers()
authorization = request.headers.get("Authorization", None)
form_class = _security.login_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
try:
if authorization is not None:
auth_header = authorization.split()
if auth_header[0] == 'Negotiate':
status, negotiate = self.negotiate_start(auth_header[1])
if status:
# Saving the first 15 characters of the kerberos key
# to encrypt/decrypt database password
session['kerberos_key'] = auth_header[1][0:15]
# Create user
retval = self.__auto_create_user(
str(negotiate.initiator_name))
elif isinstance(negotiate, Exception):
flash(gettext(negotiate), 'danger')
retval = [status,
Response(render_template(
"security/login_user.html",
login_user_form=form))]
else:
headers.add('WWW-Authenticate', 'Negotiate ' +
str(base64.b64encode(negotiate), 'utf-8'))
return False, Response("Success", 200, headers)
else:
flash(gettext("Kerberos authentication failed."
" Couldn't find kerberos ticket."), 'danger')
headers.add('WWW-Authenticate', 'Negotiate')
retval = [False,
Response(render_template(
"security/login_user.html",
login_user_form=form), 401, headers)]
finally:
if negotiate is not False:
self.negotiate_end(negotiate)
return retval
def negotiate_start(self, in_token):
svc_princ = gssapi.Name('HTTP@%s' % config.KRB_APP_HOST_NAME,
name_type=gssapi.NameType.hostbased_service)
cname = svc_princ.canonicalize(gssapi.MechType.kerberos)
try:
server_creds = gssapi.Credentials(usage='accept', name=cname)
context = gssapi.SecurityContext(creds=server_creds)
out_token = context.step(base64.b64decode(in_token))
except Exception as e:
current_app.logger.exception(e)
return False, e
if out_token and not context.complete:
return False, out_token
if context.complete:
return True, context
else:
return False, None
def negotiate_end(self, context):
# Free gss_cred_id_t
del_creds = getattr(context, 'delegated_creds', None)
if del_creds:
deleg_creds = context.delegated_creds
del(deleg_creds)
def __auto_create_user(self, username):
"""Add the ldap user to the internal SQLite database."""
username = str(username)
if config.KRB_AUTO_CREATE_USER:
user = User.query.filter_by(
username=username).first()
if user is None:
return create_user({
'username': username,
'email': username,
'role': 2,
'active': True,
'auth_source': KERBEROS
})
return True, {'username': username}

View File

@ -23,6 +23,7 @@ from .internal import BaseAuthentication
from pgadmin.model import User, ServerGroup, db, Role from pgadmin.model import User, ServerGroup, db, Role
from flask import current_app from flask import current_app
from pgadmin.tools.user_management import create_user from pgadmin.tools.user_management import create_user
from pgadmin.utils.constants import LDAP
ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}" ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}"
@ -31,6 +32,9 @@ ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}"
class LDAPAuthentication(BaseAuthentication): class LDAPAuthentication(BaseAuthentication):
"""Ldap Authentication Class""" """Ldap Authentication Class"""
def get_source_name(self):
return LDAP
def get_friendly_name(self): def get_friendly_name(self):
return gettext("ldap") return gettext("ldap")
@ -151,7 +155,7 @@ class LDAPAuthentication(BaseAuthentication):
'email': user_email, 'email': user_email,
'role': 2, 'role': 2,
'active': True, 'active': True,
'auth_source': 'ldap' 'auth_source': LDAP
}) })
return True, None return True, None

View File

@ -29,7 +29,7 @@ from flask_security.recoverable import reset_password_token_status, \
generate_reset_password_token, update_password generate_reset_password_token, update_password
from flask_security.signals import reset_password_instructions_sent from flask_security.signals import reset_password_instructions_sent
from flask_security.utils import config_value, do_flash, get_url, \ from flask_security.utils import config_value, do_flash, get_url, \
get_message, slash_url_suffix, login_user, send_mail get_message, slash_url_suffix, login_user, send_mail, logout_user
from flask_security.views import _security, _commit, _ctx from flask_security.views import _security, _commit, _ctx
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
@ -47,7 +47,8 @@ from pgadmin.utils.master_password import validate_master_password, \
set_masterpass_check_text, cleanup_master_password, get_crypt_key, \ set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
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
try: try:
from flask_security.views import default_render_json from flask_security.views import default_render_json
@ -280,7 +281,8 @@ class BrowserModule(PgAdminModule):
'browser.check_master_password', 'browser.check_master_password',
'browser.set_master_password', 'browser.set_master_password',
'browser.reset_master_password', 'browser.reset_master_password',
'browser.lock_layout'] 'browser.lock_layout'
]
blueprint = BrowserModule(MODULE_NAME, __name__) blueprint = BrowserModule(MODULE_NAME, __name__)
@ -539,6 +541,12 @@ class BrowserPluginModule(PgAdminModule):
def _get_logout_url(): def _get_logout_url():
if config.SERVER_MODE and\
session['_auth_source_manager_obj']['current_source'] == \
KERBEROS:
return '{0}?next={1}'.format(url_for(
'authenticate.kerberos_logout'), url_for(BROWSER_INDEX))
return '{0}?next={1}'.format( return '{0}?next={1}'.format(
url_for('security.logout'), url_for(BROWSER_INDEX)) url_for('security.logout'), url_for(BROWSER_INDEX))
@ -664,13 +672,18 @@ def index():
auth_only_internal = False auth_only_internal = False
auth_source = [] auth_source = []
session['allow_save_password'] = True
if config.SERVER_MODE: if config.SERVER_MODE:
if len(config.AUTHENTICATION_SOURCES) == 1\ if len(config.AUTHENTICATION_SOURCES) == 1\
and 'internal' in config.AUTHENTICATION_SOURCES: and INTERNAL in config.AUTHENTICATION_SOURCES:
auth_only_internal = True auth_only_internal = True
auth_source = session['_auth_source_manager_obj'][ auth_source = session['_auth_source_manager_obj'][
'source_friendly_name'] 'source_friendly_name']
if session['_auth_source_manager_obj']['current_source'] == KERBEROS:
session['allow_save_password'] = False
response = Response(render_template( response = Response(render_template(
MODULE_NAME + "/index.html", MODULE_NAME + "/index.html",
username=current_user.username, username=current_user.username,
@ -1086,7 +1099,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
# Check the Authentication source of the User # Check the Authentication source of the User
user = User.query.filter_by( user = User.query.filter_by(
email=form.data['email'], email=form.data['email'],
auth_source=current_app.PGADMIN_DEFAULT_AUTH_SOURCE auth_source=INTERNAL
).first() ).first()
if user is None: if user is None:

View File

@ -10,7 +10,7 @@
import simplejson as json import simplejson as json
import pgadmin.browser.server_groups as sg import pgadmin.browser.server_groups as sg
from flask import render_template, request, make_response, jsonify, \ from flask import render_template, request, make_response, jsonify, \
current_app, url_for current_app, url_for, session
from flask_babelex import gettext from flask_babelex import gettext
from flask_security import current_user, login_required from flask_security import current_user, login_required
from pgadmin.browser.server_groups.servers.types import ServerType from pgadmin.browser.server_groups.servers.types import ServerType
@ -1822,7 +1822,13 @@ class ServerNode(PGChildNodeView):
_=gettext, _=gettext,
service=server.service, service=server.service,
prompt_tunnel_password=prompt_tunnel_password, prompt_tunnel_password=prompt_tunnel_password,
prompt_password=prompt_password prompt_password=prompt_password,
allow_save_password=True if
config.ALLOW_SAVE_PASSWORD and
session['allow_save_password'] else False,
allow_save_tunnel_password=True if
config.ALLOW_SAVE_TUNNEL_PASSWORD and
session['allow_save_password'] else False
) )
) )
else: else:
@ -1836,6 +1842,9 @@ class ServerNode(PGChildNodeView):
errmsg=errmsg, errmsg=errmsg,
service=server.service, service=server.service,
_=gettext, _=gettext,
allow_save_password=True if
config.ALLOW_SAVE_PASSWORD and
session['allow_save_password'] else False,
) )
) )

View File

@ -19,7 +19,7 @@
<div class="col-sm-10"> <div class="col-sm-10">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="custom-control-input" id="save_password" name="save_password" type="checkbox" <input class="custom-control-input" id="save_password" name="save_password" type="checkbox"
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %} {% if not allow_save_password %}disabled{% endif %}
> >
<label class="custom-control-label" for="save_password">{{ _('Save Password') }}</label> <label class="custom-control-label" for="save_password">{{ _('Save Password') }}</label>
</div> </div>

View File

@ -15,7 +15,7 @@
<div class="w-100"> <div class="w-100">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="custom-control-input" id="save_tunnel_password" name="save_tunnel_password" type="checkbox" <input class="custom-control-input" id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
{% if not config.ALLOW_SAVE_TUNNEL_PASSWORD %}disabled{% endif %} {% if not allow_save_tunnel_password %}disabled{% endif %}
> >
<label class="custom-control-label" for="save_tunnel_password" class="ml-1">{{ _('Save Password') }}</label> <label class="custom-control-label" for="save_tunnel_password" class="ml-1">{{ _('Save Password') }}</label>
</div> </div>
@ -39,7 +39,7 @@
<div class="w-100"> <div class="w-100">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="custom-control-input" id="save_password" name="save_password" type="checkbox" <input class="custom-control-input" id="save_password" name="save_password" type="checkbox"
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %} {% if not allow_save_password %}disabled{% endif %}
> >
<label class="custom-control-label" for="save_password" class="ml-1">{{ _('Save Password') }}</label> <label class="custom-control-label" for="save_password" class="ml-1">{{ _('Save Password') }}</label>
</div> </div>

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block body %}
<div class="container-fluid change_pass">
<div class="row align-items-center h-100">
<div class="col-md-5"></div>
<div class="col-md-5">
<div class="panel-header h4"><i class="app-icon pg-icon-blue" aria-hidden="true"></i> {{ _('%(appname)s', appname=config.APP_NAME) }}</div>
<div class="panel-body">
<div class="d-block text-color pb-3 h5">{{ _('Login Failed.') }}</div>
<div><a href="{{ login_url }}">Click here</a> to Login again.</div>
</div>
</div>
<div class="col-md-4"></div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block body %}
<div class="container-fluid change_pass">
<div class="row align-items-center h-100">
<div class="col-md-5"></div>
<div class="col-md-5">
<div class="panel-header h4"><i class="app-icon pg-icon-blue" aria-hidden="true"></i> {{ _('%(appname)s', appname=config.APP_NAME) }}</div>
<div class="panel-body">
<div class="d-block text-color pb-3 h5">{{ _('Logged out successfully.') }}</div>
<div><a href="{{ login_url }}">Click here</a> to Login again.</div>
</div>
</div>
<div class="col-md-4"></div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,104 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import config as app_config
from pgadmin.utils.route import BaseTestGenerator
from regression.python_test_utils import test_utils as utils
from pgadmin.authenticate.registry import AuthSourceRegistry
from unittest.mock import patch, MagicMock
class KerberosLoginMockTestCase(BaseTestGenerator):
"""
This class checks Spnego/Kerberos login functionality by mocking
HTTP negotiate authentication.
"""
scenarios = [
('Spnego/Kerberos Authentication: Test Unauthorized', dict(
auth_source=['kerberos'],
auto_create_user=True,
flag=1
)),
('Spnego/Kerberos Authentication: Test Authorized', dict(
auth_source=['kerberos'],
auto_create_user=True,
flag=2
))
]
@classmethod
def setUpClass(cls):
"""
We need to logout the test client as we are testing
spnego/kerberos login scenarios.
"""
cls.tester.logout()
def setUp(self):
app_config.AUTHENTICATION_SOURCES = self.auth_source
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'kerberos'
def runTest(self):
"""This function checks spnego/kerberos login functionality."""
if self.flag == 1:
self.test_unauthorized()
elif self.flag == 2:
if app_config.SERVER_MODE is False:
self.skipTest(
"Can not run Kerberos Authentication in the Desktop mode."
)
self.test_authorized()
def test_unauthorized(self):
"""
Ensure that when client sends the first request,
the Negotiate request is sent.
"""
res = self.tester.login(None, None, True)
self.assertEqual(res.status_code, 401)
self.assertEqual(res.headers.get('www-authenticate'), 'Negotiate')
def test_authorized(self):
"""
Ensure that when the client sends an correct authorization token,
they receive a 200 OK response and the user principal is extracted and
passed on to the routed method.
"""
class delCrads:
def __init__(self):
self.initiator_name = 'user@PGADMIN.ORG'
del_crads = delCrads()
AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock(
return_value=[True, del_crads])
res = self.tester.login(None,
None,
True,
headers={'Authorization': 'Negotiate CTOKEN'}
)
self.assertEqual(res.status_code, 200)
respdata = 'Gravatar image for %s' % del_crads.initiator_name
self.assertTrue(respdata in res.data.decode('utf8'))
def tearDown(self):
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
self.tester.logout()
@classmethod
def tearDownClass(cls):
"""
We need to again login the test client as soon as test scenarios
finishes.
"""
cls.tester.logout()
app_config.AUTHENTICATION_SOURCES = ['internal']
utils.login_tester_account(cls.tester)

View File

@ -25,7 +25,7 @@ from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_json_response, bad_request, \ from pgadmin.utils.ajax import make_json_response, bad_request, \
internal_server_error, unauthorized internal_server_error, unauthorized
from config import PG_DEFAULT_DRIVER from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD
from pgadmin.model import Server, User from pgadmin.model import Server, User
from pgadmin.utils.driver import get_driver from pgadmin.utils.driver import get_driver
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
@ -402,6 +402,9 @@ def _init_query_tool(trans_id, connect, sgid, sid, did, **kwargs):
username=user, username=user,
errmsg=msg, errmsg=msg,
_=gettext, _=gettext,
allow_save_password=True if
ALLOW_SAVE_PASSWORD and
session['allow_save_password'] else False,
) )
), '', '' ), '', ''
else: else:

View File

@ -13,7 +13,7 @@ import simplejson as json
import re import re
from flask import render_template, request, \ from flask import render_template, request, \
url_for, Response, abort, current_app url_for, Response, abort, current_app, session
from flask_babelex import gettext as _ from flask_babelex import gettext as _
from flask_security import login_required, roles_required, current_user from flask_security import login_required, roles_required, current_user
from flask_security.utils import encrypt_password from flask_security.utils import encrypt_password
@ -24,7 +24,8 @@ from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response, \ 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 from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\
SUPPORTED_AUTH_SOURCES, KERBEROS
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 ServerGroup, Process, Setting
@ -167,11 +168,13 @@ def current_user_info():
config.SERVER_MODE is True config.SERVER_MODE is True
else 'postgres' else 'postgres'
), ),
allow_save_password='true' if config.ALLOW_SAVE_PASSWORD allow_save_password='true' if
config.ALLOW_SAVE_PASSWORD and session['allow_save_password']
else 'false', else 'false',
allow_save_tunnel_password='true' allow_save_tunnel_password='true' if
if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false', config.ALLOW_SAVE_TUNNEL_PASSWORD and session[
auth_sources=config.AUTHENTICATION_SOURCES, 'allow_save_password'] else 'false',
auth_sources=config.AUTHENTICATION_SOURCES
), ),
status=200, status=200,
mimetype=MIMETYPE_APP_JS mimetype=MIMETYPE_APP_JS
@ -254,10 +257,10 @@ def _create_new_user(new_data):
:return: Return new created user. :return: Return new created user.
""" """
auth_source = new_data['auth_source'] if 'auth_source' in new_data \ auth_source = new_data['auth_source'] if 'auth_source' in new_data \
else current_app.PGADMIN_DEFAULT_AUTH_SOURCE else INTERNAL
username = new_data['username'] if \ username = new_data['username'] if \
'username' in new_data and auth_source != \ 'username' in new_data and auth_source != \
current_app.PGADMIN_DEFAULT_AUTH_SOURCE else new_data['email'] INTERNAL else new_data['email']
email = new_data['email'] if 'email' in new_data else None email = new_data['email'] if 'email' in new_data else None
password = new_data['password'] if 'password' in new_data else None password = new_data['password'] if 'password' in new_data else None
@ -279,7 +282,7 @@ def _create_new_user(new_data):
def create_user(data): def create_user(data):
if 'auth_source' in data and data['auth_source'] != \ if 'auth_source' in data and data['auth_source'] != \
current_app.PGADMIN_DEFAULT_AUTH_SOURCE: INTERNAL:
req_params = ('username', 'role', 'active', 'auth_source') req_params = ('username', 'role', 'active', 'auth_source')
else: else:
req_params = ('email', 'role', 'active', 'newPassword', req_params = ('email', 'role', 'active', 'newPassword',
@ -380,7 +383,7 @@ def update(uid):
) )
# Username and email can not be changed for internal users # Username and email can not be changed for internal users
if usr.auth_source == current_app.PGADMIN_DEFAULT_AUTH_SOURCE: if usr.auth_source == INTERNAL:
non_editable_params = ('username', 'email') non_editable_params = ('username', 'email')
for f in non_editable_params: for f in non_editable_params:
@ -463,7 +466,7 @@ def role(rid):
) )
def auth_sources(): def auth_sources():
sources = [] sources = []
for source in current_app.PGADMIN_SUPPORTED_AUTH_SOURCE: for source in SUPPORTED_AUTH_SOURCES:
sources.append({'label': source, 'value': source}) sources.append({'label': source, 'value': source})
return ajax_response( return ajax_response(

View File

@ -47,3 +47,12 @@ ERROR_FETCHING_ROLE_INFORMATION = gettext(
'Error fetching role information from the database server.') 'Error fetching role information from the database server.')
ERROR_FETCHING_DATA = gettext('Unable to fetch data.') ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
# Authentication Sources
INTERNAL = 'internal'
LDAP = 'ldap'
KERBEROS = 'kerberos'
SUPPORTED_AUTH_SOURCES = [INTERNAL,
LDAP,
KERBEROS]

View File

@ -1,8 +1,9 @@
import config import config
from flask import current_app from flask import current_app, session
from flask_login import current_user from flask_login import current_user
from pgadmin.model import db, User, Server from pgadmin.model import db, User, Server
from pgadmin.utils.crypto import encrypt, decrypt from pgadmin.utils.crypto import encrypt, decrypt
from pgadmin.utils.constants import KERBEROS
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof' MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
@ -32,6 +33,11 @@ def get_crypt_key():
elif config.MASTER_PASSWORD_REQUIRED \ elif config.MASTER_PASSWORD_REQUIRED \
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 \
session['_auth_source_manager_obj']['source_friendly_name']\
== KERBEROS:
return True, session['kerberos_key'] if 'kerberos_key' in session \
else None
else: else:
return True, enc_key return True, enc_key

View File

@ -101,7 +101,8 @@ class TestClient(testing.FlaskClient):
return csrf_token return csrf_token
def login(self, email, password, _follow_redirects=False): def login(self, email, password, _follow_redirects=False,
headers=None):
if config.SERVER_MODE is True: if config.SERVER_MODE is True:
res = self.get('/login', follow_redirects=True) res = self.get('/login', follow_redirects=True)
csrf_token = self.fetch_csrf(res) csrf_token = self.fetch_csrf(res)
@ -113,7 +114,8 @@ class TestClient(testing.FlaskClient):
email=email, password=password, email=email, password=password,
csrf_token=csrf_token, csrf_token=csrf_token,
), ),
follow_redirects=_follow_redirects follow_redirects=_follow_redirects,
headers=headers
) )
self.csrf_token = csrf_token self.csrf_token = csrf_token

View File

@ -117,9 +117,9 @@ if config.SERVER_MODE is True:
app.config['WTF_CSRF_ENABLED'] = True app.config['WTF_CSRF_ENABLED'] = True
# Authentication sources # Authentication sources
app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap' app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
app.test_client_class = TestClient app.test_client_class = TestClient
test_client = app.test_client() test_client = app.test_client()
test_client.setApp(app) test_client.setApp(app)