mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -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
a60cfd3cc3
commit
c0ef0a893d
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
.git
|
||||
web/node_modules
|
||||
web/*.log
|
||||
web/regression
|
||||
web/**/tests/
|
||||
.DS_Store
|
@ -157,11 +157,15 @@ RUN apk add --no-cache --virtual \
|
||||
build-base \
|
||||
postgresql-dev \
|
||||
libffi-dev \
|
||||
krb5-dev \
|
||||
e2fsprogs-dev \
|
||||
krb5-server-ldap \
|
||||
linux-headers && \
|
||||
apk add \
|
||||
postfix \
|
||||
postgresql-client \
|
||||
postgresql-libs \
|
||||
krb5-libs \
|
||||
shadow \
|
||||
sudo \
|
||||
libcap && \
|
||||
|
@ -10,6 +10,7 @@ New features
|
||||
************
|
||||
|
||||
| `Issue #1802 <https://redmine.postgresql.org/issues/1802>`_ - Added ERD Diagram support with basic table fields, primary key, foreign key, and DDL SQL generation.
|
||||
| `Issue #5457 <https://redmine.postgresql.org/issues/5457>`_ - Added support for Kerberos authentication, using SPNEGO to forward the Kerberos tickets through a browser.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
@ -24,6 +25,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.
|
||||
|
@ -76,7 +76,7 @@ cat << EOF > "${WEBROOT}/DEBIAN/control"
|
||||
Package: ${APP_NAME}-web
|
||||
Version: ${APP_LONG_VERSION}
|
||||
Architecture: all
|
||||
Depends: ${APP_NAME}-server, apache2, libapache2-mod-wsgi-py3
|
||||
Depends: ${APP_NAME}-server, apache2, libapache2-mod-wsgi-py3, libgssapi-krb5-2
|
||||
Maintainer: pgAdmin Development Team <pgadmin-hackers@postgresql.org>
|
||||
Description: The web interface for pgAdmin, hosted under Apache HTTPD. pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.
|
||||
EOF
|
||||
|
@ -30,5 +30,5 @@ apt update
|
||||
|
||||
# Install pre-reqs
|
||||
echo "Installing build pre-requisites..."
|
||||
apt install -y build-essential python3-dev python3-venv python3-sphinx python3-wheel libpq-dev libffi-dev qtbase5-dev qt5-qmake nodejs yarn
|
||||
apt install -y build-essential python3-dev python3-venv python3-sphinx python3-wheel libpq-dev libffi-dev qtbase5-dev qt5-qmake nodejs yarn libkrb5-dev
|
||||
|
||||
|
@ -131,9 +131,9 @@ Summary: The web interface for pgAdmin, hosted under Apache HTTPD.
|
||||
License: PostgreSQL
|
||||
URL: https://www.pgadmin.org/
|
||||
%if 0%{?rhel} && 0%{?rhel} == 7
|
||||
Requires: ${APP_NAME}-server, httpd, pgadmin4-python3-mod_wsgi
|
||||
Requires: ${APP_NAME}-server, httpd, pgadmin4-python3-mod_wsgi, krb5-libs
|
||||
%else
|
||||
Requires: ${APP_NAME}-server, httpd, python3-mod_wsgi
|
||||
Requires: ${APP_NAME}-server, httpd, python3-mod_wsgi, krb5-libs
|
||||
%endif
|
||||
|
||||
%description
|
||||
|
@ -35,10 +35,10 @@ echo "Installing build pre-requisites..."
|
||||
yum groupinstall -y "Development Tools"
|
||||
|
||||
if [ ${OS_VERSION} == 7 ]; then
|
||||
yum install -y expect fakeroot httpd-devel qt5-qtbase-devel postgresql12-devel python3-devel nodejs yarn rpm-build rpm-sign yum-utils
|
||||
yum install -y expect fakeroot httpd-devel qt5-qtbase-devel postgresql12-devel python3-devel nodejs yarn rpm-build rpm-sign yum-utils krb5-devel
|
||||
pip3 install sphinx
|
||||
else
|
||||
yum install -y expect fakeroot qt5-qtbase-devel postgresql12-devel python3-devel python3-sphinx nodejs yarn rpm-build rpm-sign yum-utils
|
||||
yum install -y expect fakeroot qt5-qtbase-devel postgresql12-devel python3-devel python3-sphinx nodejs yarn rpm-build rpm-sign yum-utils krb5-devel
|
||||
fi
|
||||
|
||||
# Setup RPM macros for signing
|
||||
|
@ -43,3 +43,5 @@ cryptography<=3.0;
|
||||
sshtunnel>=0.1.5
|
||||
ldap3>=2.5.1
|
||||
Flask-BabelEx>=0.9.4
|
||||
gssapi>=1.6.11; python_version >= '3.6'
|
||||
gssapi==1.6.2; python_version <= '3.5'
|
||||
|
@ -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,7 @@ else:
|
||||
import config
|
||||
from pgadmin import create_app
|
||||
from pgadmin.utils import u_encode, fs_encoding, file_quote
|
||||
from pgadmin.utils.constants import INTERNAL
|
||||
# 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 +97,10 @@ 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
|
||||
@ -674,6 +675,7 @@ def create_app(app_name=None):
|
||||
|
||||
# Check the auth key is valid, if it's set, and we're not in server
|
||||
# mode, and it's not a help file request.
|
||||
|
||||
if not config.SERVER_MODE and app.PGADMIN_INT_KEY != '' and ((
|
||||
'key' not in request.args or
|
||||
request.args['key'] != app.PGADMIN_INT_KEY) and
|
||||
@ -695,11 +697,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")
|
||||
|
||||
|
152
web/pgadmin/authenticate/kerberos.py
Normal file
152
web/pgadmin/authenticate/kerberos.py
Normal file
@ -0,0 +1,152 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""A blueprint module implementing the Spnego/Kerberos authentication."""
|
||||
|
||||
import base64
|
||||
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
|
||||
from pgadmin.tools.user_management import create_user
|
||||
from pgadmin.utils.constants import KERBEROS
|
||||
|
||||
from flask_security.views import _security
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
from .internal import BaseAuthentication
|
||||
|
||||
try:
|
||||
import gssapi
|
||||
KERBEROS_AUTH_AVAILABLE = True
|
||||
except ImportError:
|
||||
# Do not fail at this time, as this could be a desktop mode.
|
||||
# Instead throw the runtime error, when the server attempts
|
||||
# to use this authentication method.
|
||||
KERBEROS_AUTH_AVAILABLE = False
|
||||
|
||||
# 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):
|
||||
|
||||
if KERBEROS_AUTH_AVAILABLE is not True:
|
||||
raise RuntimeError(gettext(
|
||||
"Kerberos authentication can't be used as"
|
||||
" GSSAPI module couldn't be loaded."
|
||||
))
|
||||
|
||||
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,
|
||||
@ -1087,7 +1100,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 - 2020, 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,7 +117,6 @@ 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
|
||||
|
Loading…
Reference in New Issue
Block a user