public inbox for [email protected]
help / color / mirror / Atom feedFrom: Khushboo Vashi <[email protected]>
To: Akshay Joshi <[email protected]>
Cc: Aditya Toshniwal <[email protected]>
Cc: pgadmin-hackers <[email protected]>
Subject: Re: [pgAdmin4][Patch] - RM 5457 - Kerberos Authentication - Phase 1
Date: Thu, 14 Jan 2021 12:17:57 +0530
Message-ID: <CAFOhELeF7kd6JY_kEEJRr7osc8kmUeJOyDPnO3knynAf8MTaSw@mail.gmail.com> (raw)
In-Reply-To: <CANxoLDcXZnR6L3QMRoBDPqkm_Qaqvj6OYshk4anZ6DMGfk8UJQ@mail.gmail.com>
References: <CAFOhELdXhWMR2zS4dnH+SudN0s7LiENH+vczC0YhuifPgm+G5g@mail.gmail.com>
<CANxoLDeKWqKP-6=KRTY_-vDSZv2g=P7dKJkzWPS7aOB3EoZOoA@mail.gmail.com>
<CAM9w-_koVr9Qy3hDaSaDC9pX6jeDzj5gSghYjT2yxQ2h6zbr=w@mail.gmail.com>
<CAFOhELeFZAXSWQYFoRGvfOEZ+Kt_sWWxPFte17o-maqq0JshXg@mail.gmail.com>
<CANxoLDcXZnR6L3QMRoBDPqkm_Qaqvj6OYshk4anZ6DMGfk8UJQ@mail.gmail.com>
Hi,
Please find the attached updated patch.
Thanks,
Khushboo
On Thu, Jan 14, 2021 at 12:00 PM Akshay Joshi <[email protected]>
wrote:
> Hi Khushboo
>
> Seems you have attached the wrong patch. Please send the updated patch.
>
> On Wed, Jan 13, 2021 at 2:35 PM Khushboo Vashi <
> [email protected]> wrote:
>
>> Hi,
>>
>> Please find the attached updated patch.
>>
>> Thanks,
>> Khushboo
>>
>> On Fri, Jan 1, 2021 at 1:07 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Khushboo,
>>>
>>> I've just done the code review. Apart from below, the patch looks good
>>> to me:
>>>
>>> 1) Move the auth source constants -ldap, kerberos out of app object.
>>> They don't belong there. You can create the constants somewhere else and
>>> import them.
>>>
>>> +app.PGADMIN_LDAP_AUTH_SOURCE = 'ldap'
>>>
>>> +app.PGADMIN_KERBEROS_AUTH_SOURCE = 'kerberos'
>>>
>>>
>>> Done
>>
>>> 2) Are we going to make kerberos default for wsgi ?
>>>
>>> *--- a/web/pgAdmin4.wsgi*
>>>
>>> *+++ b/web/pgAdmin4.wsgi*
>>>
>>> @@ -24,6 +24,10 @@ builtins.SERVER_MODE = True
>>>
>>>
>>>
>>> import config
>>>
>>>
>>>
>>> +
>>>
>>> +config.AUTHENTICATION_SOURCES = ['kerberos']
>>>
>>> +config.KERBEROS_AUTO_CREATE_USER = True
>>>
>>> +
>>>
>>>
>>> Removed, it was only for testing.
>>
>>> 3) Remove the commented code.
>>>
>>> + # if self.form.data['email'] and
>>> self.form.data['password'] and \
>>>
>>> + # source.get_source_name() ==\
>>>
>>> + # current_app.PGADMIN_KERBEROS_AUTH_SOURCE:
>>>
>>> + # continue
>>>
>>>
>>> Removed the comment, it is actually the part of the code.
>>
>>> 4) KERBEROSAuthentication could be KerberosAuthentication
>>>
>>> class KERBEROSAuthentication(BaseAuthentication):
>>>
>>>
>>> Done.
>>
>>> 5) You can use the constants (ldap, kerberos) you had defined when
>>> creating a user.
>>>
>>> + 'auth_source': 'kerberos'
>>>
>>>
>>> Done.
>>
>>> 6) The below URLs belong to the authenticate module. Currently they are
>>> in the browser module. I would also suggest rephrasing the URL from
>>> /kerberos_login to /login/kerberos. Same for logout.
>>>
>> Done the rephrasing as well as moved to the authentication module.
>>
>>
>>> Also, even though the method GET works, we should use the POST method
>>> for login and DELETE for logout.
>>>
>> Kerberos_login just redirects the page to the actual login, so no need
>> for the POST method.
>> I followed the same method for the Logout user we have used for the
>> normal user.
>>
>>
>>> [email protected]("/kerberos_login",
>>>
>>> + endpoint="kerberos_login", methods=["GET"])
>>>
>>>
>>> [email protected]("/kerberos_logout",
>>>
>>> + endpoint="kerberos_logout", methods=["GET"])
>>>
>>>
>>>
>>>
>>
>>> On Tue, Dec 22, 2020 at 6:07 PM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Hi Aditya
>>>>
>>>> Can you please do the code review?
>>>>
>>>> On Tue, Dec 22, 2020 at 3:44 PM Khushboo Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> Please find the attached patch to support Kerberos Authentication in
>>>>> pgAdmin RM 5457.
>>>>>
>>>>> The patch introduces a new pluggable option for Kerberos
>>>>> authentication, using SPNEGO to forward kerberos tickets through a browser
>>>>> which will bypass the login page entirely if the Kerberos Authentication
>>>>> succeeds.
>>>>>
>>>>> The complete setup of the Kerberos Server + pgAdmin Server + Client is
>>>>> documented in a separate file and attached.
>>>>>
>>>>> This patch also includes the small fix related to logging #5829
>>>>>
>>>>> Thanks,
>>>>> Khushboo
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>
Attachments:
[application/octet-stream] RM_5457_v2.patch (37.7K, 3-RM_5457_v2.patch)
download | inline diff:
diff --git a/web/config.py b/web/config.py
index 2b314fe69..d02a91380 100644
--- a/web/config.py
+++ b/web/config.py
@@ -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
##########################################################################
diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py
index ff9c00f50..14afe7dc1 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -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
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index dae0b8cd2..a73335371 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -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()
diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py
index 7ede73cd8..1fdb66cf7 100644
--- a/web/pgadmin/authenticate/__init__.py
+++ b/web/pgadmin/authenticate/__init__.py
@@ -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='')
[email protected]("/login/kerberos",
+ endpoint="kerberos_login", methods=["GET"])
[email protected]
+def kerberos_login():
+ logout_user()
+ return Response(render_template("browser/kerberos_login.html",
+ login_url=url_for('security.login'),
+ ))
+
+
[email protected]("/logout/kerberos",
+ endpoint="kerberos_logout", methods=["GET"])
[email protected]
+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
diff --git a/web/pgadmin/authenticate/internal.py b/web/pgadmin/authenticate/internal.py
index 804a487c7..484a7fdca 100644
--- a/web/pgadmin/authenticate/internal.py
+++ b/web/pgadmin/authenticate/internal.py
@@ -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")
diff --git a/web/pgadmin/authenticate/kerberos.py b/web/pgadmin/authenticate/kerberos.py
new file mode 100644
index 000000000..629fc7bf7
--- /dev/null
+++ b/web/pgadmin/authenticate/kerberos.py
@@ -0,0 +1,138 @@
+##########################################################################
+#
+# 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
+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}
diff --git a/web/pgadmin/authenticate/ldap.py b/web/pgadmin/authenticate/ldap.py
index a9eca110f..2f0f61b7c 100644
--- a/web/pgadmin/authenticate/ldap.py
+++ b/web/pgadmin/authenticate/ldap.py
@@ -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
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 1bae28f9c..c0ad869a1 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -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:
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index ecc1281a2..5daef8120 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -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,
)
)
diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/password.html b/web/pgadmin/browser/server_groups/servers/templates/servers/password.html
index 9b2c425e3..35f4e2a16 100644
--- a/web/pgadmin/browser/server_groups/servers/templates/servers/password.html
+++ b/web/pgadmin/browser/server_groups/servers/templates/servers/password.html
@@ -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>
diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html b/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html
index 5de642f85..e34a257f2 100644
--- a/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html
+++ b/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html
@@ -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>
diff --git a/web/pgadmin/browser/templates/browser/kerberos_login.html b/web/pgadmin/browser/templates/browser/kerberos_login.html
new file mode 100644
index 000000000..c112e3196
--- /dev/null
+++ b/web/pgadmin/browser/templates/browser/kerberos_login.html
@@ -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 %}
diff --git a/web/pgadmin/browser/templates/browser/kerberos_logout.html b/web/pgadmin/browser/templates/browser/kerberos_logout.html
new file mode 100644
index 000000000..430dc6f25
--- /dev/null
+++ b/web/pgadmin/browser/templates/browser/kerberos_logout.html
@@ -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 %}
diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py
new file mode 100644
index 000000000..f87ce5521
--- /dev/null
+++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py
@@ -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 = '[email protected]'
+ 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)
diff --git a/web/pgadmin/tools/datagrid/__init__.py b/web/pgadmin/tools/datagrid/__init__.py
index 2405a498d..05ed998c6 100644
--- a/web/pgadmin/tools/datagrid/__init__.py
+++ b/web/pgadmin/tools/datagrid/__init__.py
@@ -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:
diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py
index 8641130c4..ce280a3d2 100644
--- a/web/pgadmin/tools/user_management/__init__.py
+++ b/web/pgadmin/tools/user_management/__init__.py
@@ -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(
diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py
index 0a2261f05..5fd942304 100644
--- a/web/pgadmin/utils/constants.py
+++ b/web/pgadmin/utils/constants.py
@@ -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]
diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py
index 759bf36e0..629eec941 100644
--- a/web/pgadmin/utils/master_password.py
+++ b/web/pgadmin/utils/master_password.py
@@ -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
diff --git a/web/regression/python_test_utils/csrf_test_client.py b/web/regression/python_test_utils/csrf_test_client.py
index 11d2cfca5..ca4120e18 100644
--- a/web/regression/python_test_utils/csrf_test_client.py
+++ b/web/regression/python_test_utils/csrf_test_client.py
@@ -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
diff --git a/web/regression/runtests.py b/web/regression/runtests.py
index 3328ed3f6..9b794e41f 100644
--- a/web/regression/runtests.py
+++ b/web/regression/runtests.py
@@ -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)
view thread (32+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected]
Subject: Re: [pgAdmin4][Patch] - RM 5457 - Kerberos Authentication - Phase 1
In-Reply-To: <CAFOhELeF7kd6JY_kEEJRr7osc8kmUeJOyDPnO3knynAf8MTaSw@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox