mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-26 02:30:21 -06:00
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:
parent
9a47e574e3
commit
6ead597b43
@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
|
||||
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
|
||||
************
|
||||
@ -23,6 +24,7 @@ Bug fixes
|
||||
| `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 #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 #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.
|
||||
|
@ -43,3 +43,4 @@ cryptography<=3.0;
|
||||
sshtunnel>=0.1.5
|
||||
ldap3>=2.5.1
|
||||
Flask-BabelEx>=0.9.4
|
||||
gssapi>=1.6.11
|
||||
|
@ -535,7 +535,7 @@ ENHANCED_COOKIE_PROTECTION = True
|
||||
##########################################################################
|
||||
|
||||
# Default setting is internal
|
||||
# External Supported Sources: ldap
|
||||
# External Supported Sources: ldap, kerberos
|
||||
# Multiple authentication can be achieved by setting this parameter to
|
||||
# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first,
|
||||
# in case of failure internal authentication will be done.
|
||||
@ -618,6 +618,26 @@ LDAP_CA_CERT_FILE = ''
|
||||
LDAP_CERT_FILE = ''
|
||||
LDAP_KEY_FILE = ''
|
||||
|
||||
|
||||
##########################################################################
|
||||
# Kerberos Configuration
|
||||
##########################################################################
|
||||
|
||||
KRB_APP_HOST_NAME = DEFAULT_SERVER
|
||||
|
||||
# If the default_keytab_name is not set in krb5.conf or
|
||||
# the KRB_KTNAME environment variable is not set then, explicitly set
|
||||
# the Keytab file
|
||||
|
||||
KRB_KTNAME = '<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
|
||||
##########################################################################
|
||||
|
@ -35,6 +35,9 @@ else:
|
||||
import config
|
||||
from pgadmin import create_app
|
||||
from pgadmin.utils import u_encode, fs_encoding, file_quote
|
||||
from pgadmin.utils.constants import INTERNAL, LDAP,\
|
||||
KERBEROS, SUPPORTED_AUTH_SOURCES
|
||||
|
||||
# Get the config database schema version. We store this in pgadmin.model
|
||||
# as it turns out that putting it in the config files isn't a great idea
|
||||
from pgadmin.model import SCHEMA_VERSION
|
||||
@ -96,15 +99,11 @@ if config.SERVER_MODE:
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
|
||||
# Authentication sources
|
||||
app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
|
||||
app.PGADMIN_SUPPORTED_AUTH_SOURCE = ['internal', 'ldap']
|
||||
|
||||
if len(config.AUTHENTICATION_SOURCES) > 0:
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = config.AUTHENTICATION_SOURCES[0]
|
||||
else:
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = app.PGADMIN_DEFAULT_AUTH_SOURCE
|
||||
|
||||
app.logger.debug(
|
||||
"Authentication Source: %s" % app.PGADMIN_DEFAULT_AUTH_SOURCE)
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
|
||||
|
||||
# Start the web server. The port number should have already been set by the
|
||||
# runtime if we're running in desktop mode, otherwise we'll just use the
|
||||
|
@ -43,6 +43,7 @@ from pgadmin.utils.ajax import internal_server_error, make_json_response
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
from pgadmin import authenticate
|
||||
from pgadmin.utils.security_headers import SecurityHeaders
|
||||
from pgadmin.utils.constants import KERBEROS
|
||||
|
||||
# Explicitly set the mime-types so that a corrupted windows registry will not
|
||||
# affect pgAdmin 4 to be load properly. This will avoid the issues that may
|
||||
@ -695,11 +696,19 @@ def create_app(app_name=None):
|
||||
)
|
||||
abort(401)
|
||||
login_user(user)
|
||||
elif config.SERVER_MODE and\
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\
|
||||
KERBEROS and \
|
||||
not current_user.is_authenticated and \
|
||||
request.endpoint in ('redirects.index', 'security.login'):
|
||||
return authenticate.login()
|
||||
|
||||
# if the server is restarted the in memory key will be lost
|
||||
# but the user session may still be active. Logout the user
|
||||
# to get the key again when login
|
||||
if config.SERVER_MODE and current_user.is_authenticated and \
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
|
||||
KERBEROS and \
|
||||
current_app.keyManager.get() is None and \
|
||||
request.endpoint not in ('security.login', 'security.logout'):
|
||||
logout_user()
|
||||
|
@ -11,16 +11,21 @@
|
||||
|
||||
import flask
|
||||
import pickle
|
||||
from flask import current_app, flash
|
||||
from flask import current_app, flash, Response, request, url_for,\
|
||||
render_template
|
||||
from flask_babelex import gettext
|
||||
from flask_security import current_user
|
||||
from flask_security.views import _security, _ctx
|
||||
from flask_security.utils import config_value, get_post_logout_redirect, \
|
||||
get_post_login_redirect
|
||||
get_post_login_redirect, logout_user
|
||||
|
||||
from flask import session
|
||||
|
||||
import config
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils.constants import KERBEROS
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
|
||||
from .registry import AuthSourceRegistry
|
||||
|
||||
MODULE_NAME = 'authenticate'
|
||||
@ -28,12 +33,34 @@ MODULE_NAME = 'authenticate'
|
||||
|
||||
class AuthenticateModule(PgAdminModule):
|
||||
def get_exposed_url_endpoints(self):
|
||||
return ['authenticate.login']
|
||||
return ['authenticate.login',
|
||||
'authenticate.kerberos_login',
|
||||
'authenticate.kerberos_logout']
|
||||
|
||||
|
||||
blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
|
||||
|
||||
|
||||
@blueprint.route("/login/kerberos",
|
||||
endpoint="kerberos_login", methods=["GET"])
|
||||
@pgCSRFProtect.exempt
|
||||
def kerberos_login():
|
||||
logout_user()
|
||||
return Response(render_template("browser/kerberos_login.html",
|
||||
login_url=url_for('security.login'),
|
||||
))
|
||||
|
||||
|
||||
@blueprint.route("/logout/kerberos",
|
||||
endpoint="kerberos_logout", methods=["GET"])
|
||||
@pgCSRFProtect.exempt
|
||||
def kerberos_logout():
|
||||
logout_user()
|
||||
return Response(render_template("browser/kerberos_logout.html",
|
||||
login_url=url_for('security.login'),
|
||||
))
|
||||
|
||||
|
||||
@blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""
|
||||
@ -56,15 +83,24 @@ def login():
|
||||
if status:
|
||||
# Login the user
|
||||
status, msg = auth_obj.login()
|
||||
current_auth_obj = auth_obj.as_dict()
|
||||
if not status:
|
||||
if current_auth_obj['current_source'] ==\
|
||||
KERBEROS:
|
||||
return flask.redirect('{0}?next={1}'.format(url_for(
|
||||
'authenticate.kerberos_login'), url_for('browser.index')))
|
||||
|
||||
flash(gettext(msg), 'danger')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
|
||||
session['_auth_source_manager_obj'] = auth_obj.as_dict()
|
||||
session['_auth_source_manager_obj'] = current_auth_obj
|
||||
return flask.redirect(get_post_login_redirect())
|
||||
|
||||
elif isinstance(msg, Response):
|
||||
return msg
|
||||
flash(gettext(msg), 'danger')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
response = flask.redirect(get_post_logout_redirect())
|
||||
return response
|
||||
|
||||
|
||||
class AuthSourceManager():
|
||||
@ -75,6 +111,7 @@ class AuthSourceManager():
|
||||
self.auth_sources = sources
|
||||
self.source = None
|
||||
self.source_friendly_name = None
|
||||
self.current_source = None
|
||||
|
||||
def as_dict(self):
|
||||
"""
|
||||
@ -84,9 +121,17 @@ class AuthSourceManager():
|
||||
res = dict()
|
||||
res['source_friendly_name'] = self.source_friendly_name
|
||||
res['auth_sources'] = self.auth_sources
|
||||
res['current_source'] = self.current_source
|
||||
|
||||
return res
|
||||
|
||||
def set_current_source(self, source):
|
||||
self.current_source = source
|
||||
|
||||
@property
|
||||
def get_current_source(self, source):
|
||||
return self.current_source
|
||||
|
||||
def set_source(self, source):
|
||||
self.source = source
|
||||
|
||||
@ -115,9 +160,33 @@ class AuthSourceManager():
|
||||
msg = None
|
||||
for src in self.auth_sources:
|
||||
source = get_auth_sources(src)
|
||||
current_app.logger.debug(
|
||||
"Authentication initiated via source: %s" %
|
||||
source.get_source_name())
|
||||
|
||||
if self.form.data['email'] and self.form.data['password'] and \
|
||||
source.get_source_name() == KERBEROS:
|
||||
continue
|
||||
|
||||
status, msg = source.authenticate(self.form)
|
||||
|
||||
# When server sends Unauthorized header to get the ticket over HTTP
|
||||
# OR When kerberos authentication failed while accessing pgadmin,
|
||||
# we need to break the loop as no need to authenticate further
|
||||
# even if the authentication sources set to multiple
|
||||
if not status:
|
||||
if (hasattr(msg, 'status') and
|
||||
msg.status == '401 UNAUTHORIZED') or\
|
||||
(source.get_source_name() ==
|
||||
KERBEROS and
|
||||
request.method == 'GET'):
|
||||
break
|
||||
|
||||
if status:
|
||||
self.set_source(source)
|
||||
self.set_current_source(source.get_source_name())
|
||||
if msg is not None and 'username' in msg:
|
||||
self.form._fields['email'].data = msg['username']
|
||||
return status, msg
|
||||
return status, msg
|
||||
|
||||
@ -125,6 +194,9 @@ class AuthSourceManager():
|
||||
status, msg = self.source.login(self.form)
|
||||
if status:
|
||||
self.set_source_friendly_name(self.source.get_friendly_name())
|
||||
current_app.logger.debug(
|
||||
"Authentication and Login successfully done via source : %s" %
|
||||
self.source.get_source_name())
|
||||
return status, msg
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@ from flask_babelex import gettext
|
||||
from .registry import AuthSourceRegistry
|
||||
from pgadmin.model import User
|
||||
from pgadmin.utils.validation_utils import validate_email
|
||||
from pgadmin.utils.constants import INTERNAL
|
||||
|
||||
|
||||
@six.add_metaclass(AuthSourceRegistry)
|
||||
@ -31,7 +32,11 @@ class BaseAuthentication(object):
|
||||
'INVALID_EMAIL': gettext('Email/Username is not valid')
|
||||
}
|
||||
|
||||
@abstractproperty
|
||||
@abstractmethod
|
||||
def get_source_name(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_friendly_name(self):
|
||||
pass
|
||||
|
||||
@ -82,6 +87,9 @@ class BaseAuthentication(object):
|
||||
|
||||
class InternalAuthentication(BaseAuthentication):
|
||||
|
||||
def get_source_name(self):
|
||||
return INTERNAL
|
||||
|
||||
def get_friendly_name(self):
|
||||
return gettext("internal")
|
||||
|
||||
|
138
web/pgadmin/authenticate/kerberos.py
Normal file
138
web/pgadmin/authenticate/kerberos.py
Normal 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}
|
@ -23,6 +23,7 @@ from .internal import BaseAuthentication
|
||||
from pgadmin.model import User, ServerGroup, db, Role
|
||||
from flask import current_app
|
||||
from pgadmin.tools.user_management import create_user
|
||||
from pgadmin.utils.constants import LDAP
|
||||
|
||||
|
||||
ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}"
|
||||
@ -31,6 +32,9 @@ ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}"
|
||||
class LDAPAuthentication(BaseAuthentication):
|
||||
"""Ldap Authentication Class"""
|
||||
|
||||
def get_source_name(self):
|
||||
return LDAP
|
||||
|
||||
def get_friendly_name(self):
|
||||
return gettext("ldap")
|
||||
|
||||
@ -151,7 +155,7 @@ class LDAPAuthentication(BaseAuthentication):
|
||||
'email': user_email,
|
||||
'role': 2,
|
||||
'active': True,
|
||||
'auth_source': 'ldap'
|
||||
'auth_source': LDAP
|
||||
})
|
||||
|
||||
return True, None
|
||||
|
@ -29,7 +29,7 @@ from flask_security.recoverable import reset_password_token_status, \
|
||||
generate_reset_password_token, update_password
|
||||
from flask_security.signals import reset_password_instructions_sent
|
||||
from flask_security.utils import config_value, do_flash, get_url, \
|
||||
get_message, slash_url_suffix, login_user, send_mail
|
||||
get_message, slash_url_suffix, login_user, send_mail, logout_user
|
||||
from flask_security.views import _security, _commit, _ctx
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
@ -47,7 +47,8 @@ from pgadmin.utils.master_password import validate_master_password, \
|
||||
set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
|
||||
set_crypt_key, process_masterpass_disabled
|
||||
from pgadmin.model import User
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\
|
||||
INTERNAL, KERBEROS
|
||||
|
||||
try:
|
||||
from flask_security.views import default_render_json
|
||||
@ -280,7 +281,8 @@ class BrowserModule(PgAdminModule):
|
||||
'browser.check_master_password',
|
||||
'browser.set_master_password',
|
||||
'browser.reset_master_password',
|
||||
'browser.lock_layout']
|
||||
'browser.lock_layout'
|
||||
]
|
||||
|
||||
|
||||
blueprint = BrowserModule(MODULE_NAME, __name__)
|
||||
@ -539,6 +541,12 @@ class BrowserPluginModule(PgAdminModule):
|
||||
|
||||
|
||||
def _get_logout_url():
|
||||
if config.SERVER_MODE and\
|
||||
session['_auth_source_manager_obj']['current_source'] == \
|
||||
KERBEROS:
|
||||
return '{0}?next={1}'.format(url_for(
|
||||
'authenticate.kerberos_logout'), url_for(BROWSER_INDEX))
|
||||
|
||||
return '{0}?next={1}'.format(
|
||||
url_for('security.logout'), url_for(BROWSER_INDEX))
|
||||
|
||||
@ -664,13 +672,18 @@ def index():
|
||||
auth_only_internal = False
|
||||
auth_source = []
|
||||
|
||||
session['allow_save_password'] = True
|
||||
|
||||
if config.SERVER_MODE:
|
||||
if len(config.AUTHENTICATION_SOURCES) == 1\
|
||||
and 'internal' in config.AUTHENTICATION_SOURCES:
|
||||
and INTERNAL in config.AUTHENTICATION_SOURCES:
|
||||
auth_only_internal = True
|
||||
auth_source = session['_auth_source_manager_obj'][
|
||||
'source_friendly_name']
|
||||
|
||||
if session['_auth_source_manager_obj']['current_source'] == KERBEROS:
|
||||
session['allow_save_password'] = False
|
||||
|
||||
response = Response(render_template(
|
||||
MODULE_NAME + "/index.html",
|
||||
username=current_user.username,
|
||||
@ -1086,7 +1099,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
# Check the Authentication source of the User
|
||||
user = User.query.filter_by(
|
||||
email=form.data['email'],
|
||||
auth_source=current_app.PGADMIN_DEFAULT_AUTH_SOURCE
|
||||
auth_source=INTERNAL
|
||||
).first()
|
||||
|
||||
if user is None:
|
||||
|
@ -10,7 +10,7 @@
|
||||
import simplejson as json
|
||||
import pgadmin.browser.server_groups as sg
|
||||
from flask import render_template, request, make_response, jsonify, \
|
||||
current_app, url_for
|
||||
current_app, url_for, session
|
||||
from flask_babelex import gettext
|
||||
from flask_security import current_user, login_required
|
||||
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||
@ -1822,7 +1822,13 @@ class ServerNode(PGChildNodeView):
|
||||
_=gettext,
|
||||
service=server.service,
|
||||
prompt_tunnel_password=prompt_tunnel_password,
|
||||
prompt_password=prompt_password
|
||||
prompt_password=prompt_password,
|
||||
allow_save_password=True if
|
||||
config.ALLOW_SAVE_PASSWORD and
|
||||
session['allow_save_password'] else False,
|
||||
allow_save_tunnel_password=True if
|
||||
config.ALLOW_SAVE_TUNNEL_PASSWORD and
|
||||
session['allow_save_password'] else False
|
||||
)
|
||||
)
|
||||
else:
|
||||
@ -1836,6 +1842,9 @@ class ServerNode(PGChildNodeView):
|
||||
errmsg=errmsg,
|
||||
service=server.service,
|
||||
_=gettext,
|
||||
allow_save_password=True if
|
||||
config.ALLOW_SAVE_PASSWORD and
|
||||
session['allow_save_password'] else False,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
<div class="col-sm-10">
|
||||
<div class="custom-control custom-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>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div class="w-100">
|
||||
<div class="custom-control custom-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>
|
||||
</div>
|
||||
@ -39,7 +39,7 @@
|
||||
<div class="w-100">
|
||||
<div class="custom-control custom-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>
|
||||
</div>
|
||||
|
16
web/pgadmin/browser/templates/browser/kerberos_login.html
Normal file
16
web/pgadmin/browser/templates/browser/kerberos_login.html
Normal 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 %}
|
16
web/pgadmin/browser/templates/browser/kerberos_logout.html
Normal file
16
web/pgadmin/browser/templates/browser/kerberos_logout.html
Normal 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 %}
|
104
web/pgadmin/browser/tests/test_kerberos_with_mocking.py
Normal file
104
web/pgadmin/browser/tests/test_kerberos_with_mocking.py
Normal 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)
|
@ -25,7 +25,7 @@ from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
|
||||
internal_server_error, unauthorized
|
||||
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD
|
||||
from pgadmin.model import Server, User
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
|
||||
@ -402,6 +402,9 @@ def _init_query_tool(trans_id, connect, sgid, sid, did, **kwargs):
|
||||
username=user,
|
||||
errmsg=msg,
|
||||
_=gettext,
|
||||
allow_save_password=True if
|
||||
ALLOW_SAVE_PASSWORD and
|
||||
session['allow_save_password'] else False,
|
||||
)
|
||||
), '', ''
|
||||
else:
|
||||
|
@ -13,7 +13,7 @@ import simplejson as json
|
||||
import re
|
||||
|
||||
from flask import render_template, request, \
|
||||
url_for, Response, abort, current_app
|
||||
url_for, Response, abort, current_app, session
|
||||
from flask_babelex import gettext as _
|
||||
from flask_security import login_required, roles_required, current_user
|
||||
from flask_security.utils import encrypt_password
|
||||
@ -24,7 +24,8 @@ from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils.ajax import make_response as ajax_response, \
|
||||
make_json_response, bad_request, internal_server_error, forbidden
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\
|
||||
SUPPORTED_AUTH_SOURCES, KERBEROS
|
||||
from pgadmin.utils.validation_utils import validate_email
|
||||
from pgadmin.model import db, Role, User, UserPreference, Server, \
|
||||
ServerGroup, Process, Setting
|
||||
@ -167,11 +168,13 @@ def current_user_info():
|
||||
config.SERVER_MODE is True
|
||||
else 'postgres'
|
||||
),
|
||||
allow_save_password='true' if config.ALLOW_SAVE_PASSWORD
|
||||
allow_save_password='true' if
|
||||
config.ALLOW_SAVE_PASSWORD and session['allow_save_password']
|
||||
else 'false',
|
||||
allow_save_tunnel_password='true'
|
||||
if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false',
|
||||
auth_sources=config.AUTHENTICATION_SOURCES,
|
||||
allow_save_tunnel_password='true' if
|
||||
config.ALLOW_SAVE_TUNNEL_PASSWORD and session[
|
||||
'allow_save_password'] else 'false',
|
||||
auth_sources=config.AUTHENTICATION_SOURCES
|
||||
),
|
||||
status=200,
|
||||
mimetype=MIMETYPE_APP_JS
|
||||
@ -254,10 +257,10 @@ def _create_new_user(new_data):
|
||||
:return: Return new created user.
|
||||
"""
|
||||
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
|
||||
else current_app.PGADMIN_DEFAULT_AUTH_SOURCE
|
||||
else INTERNAL
|
||||
username = new_data['username'] if \
|
||||
'username' in new_data and auth_source != \
|
||||
current_app.PGADMIN_DEFAULT_AUTH_SOURCE else new_data['email']
|
||||
INTERNAL else new_data['email']
|
||||
email = new_data['email'] if 'email' in new_data else None
|
||||
password = new_data['password'] if 'password' in new_data else None
|
||||
|
||||
@ -279,7 +282,7 @@ def _create_new_user(new_data):
|
||||
|
||||
def create_user(data):
|
||||
if 'auth_source' in data and data['auth_source'] != \
|
||||
current_app.PGADMIN_DEFAULT_AUTH_SOURCE:
|
||||
INTERNAL:
|
||||
req_params = ('username', 'role', 'active', 'auth_source')
|
||||
else:
|
||||
req_params = ('email', 'role', 'active', 'newPassword',
|
||||
@ -380,7 +383,7 @@ def update(uid):
|
||||
)
|
||||
|
||||
# Username and email can not be changed for internal users
|
||||
if usr.auth_source == current_app.PGADMIN_DEFAULT_AUTH_SOURCE:
|
||||
if usr.auth_source == INTERNAL:
|
||||
non_editable_params = ('username', 'email')
|
||||
|
||||
for f in non_editable_params:
|
||||
@ -463,7 +466,7 @@ def role(rid):
|
||||
)
|
||||
def auth_sources():
|
||||
sources = []
|
||||
for source in current_app.PGADMIN_SUPPORTED_AUTH_SOURCE:
|
||||
for source in SUPPORTED_AUTH_SOURCES:
|
||||
sources.append({'label': source, 'value': source})
|
||||
|
||||
return ajax_response(
|
||||
|
@ -47,3 +47,12 @@ ERROR_FETCHING_ROLE_INFORMATION = gettext(
|
||||
'Error fetching role information from the database server.')
|
||||
|
||||
ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
|
||||
|
||||
# Authentication Sources
|
||||
INTERNAL = 'internal'
|
||||
LDAP = 'ldap'
|
||||
KERBEROS = 'kerberos'
|
||||
|
||||
SUPPORTED_AUTH_SOURCES = [INTERNAL,
|
||||
LDAP,
|
||||
KERBEROS]
|
||||
|
@ -1,8 +1,9 @@
|
||||
import config
|
||||
from flask import current_app
|
||||
from flask import current_app, session
|
||||
from flask_login import current_user
|
||||
from pgadmin.model import db, User, Server
|
||||
from pgadmin.utils.crypto import encrypt, decrypt
|
||||
from pgadmin.utils.constants import KERBEROS
|
||||
|
||||
|
||||
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
|
||||
@ -32,6 +33,11 @@ def get_crypt_key():
|
||||
elif config.MASTER_PASSWORD_REQUIRED \
|
||||
and not config.SERVER_MODE and enc_key is None:
|
||||
return False, None
|
||||
elif config.SERVER_MODE and \
|
||||
session['_auth_source_manager_obj']['source_friendly_name']\
|
||||
== KERBEROS:
|
||||
return True, session['kerberos_key'] if 'kerberos_key' in session \
|
||||
else None
|
||||
else:
|
||||
return True, enc_key
|
||||
|
||||
|
@ -101,7 +101,8 @@ class TestClient(testing.FlaskClient):
|
||||
|
||||
return csrf_token
|
||||
|
||||
def login(self, email, password, _follow_redirects=False):
|
||||
def login(self, email, password, _follow_redirects=False,
|
||||
headers=None):
|
||||
if config.SERVER_MODE is True:
|
||||
res = self.get('/login', follow_redirects=True)
|
||||
csrf_token = self.fetch_csrf(res)
|
||||
@ -113,7 +114,8 @@ class TestClient(testing.FlaskClient):
|
||||
email=email, password=password,
|
||||
csrf_token=csrf_token,
|
||||
),
|
||||
follow_redirects=_follow_redirects
|
||||
follow_redirects=_follow_redirects,
|
||||
headers=headers
|
||||
)
|
||||
self.csrf_token = csrf_token
|
||||
|
||||
|
@ -117,9 +117,9 @@ if config.SERVER_MODE is True:
|
||||
app.config['WTF_CSRF_ENABLED'] = True
|
||||
|
||||
# Authentication sources
|
||||
app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
|
||||
|
||||
|
||||
app.test_client_class = TestClient
|
||||
test_client = app.test_client()
|
||||
test_client.setApp(app)
|
||||
|
Loading…
Reference in New Issue
Block a user