1) Added support for authentication via the web server (REMOTE_USER). Fixes #6657

2) Fixed OAuth2 integration redirect issue. Fixes #6719

Initial patch for 6657 sent by: Tom Schreiber
This commit is contained in:
Khushboo Vashi 2021-10-12 14:52:30 +05:30 committed by Akshay Joshi
parent ca40add29b
commit a726635290
14 changed files with 282 additions and 9 deletions

View File

@ -39,6 +39,7 @@ Mode is pre-configured for security.
ldap
kerberos
oauth2
webserver
.. note:: Pre-compiled and configured installation packages are available for

View File

@ -11,6 +11,7 @@ New features
| `Issue #6081 <https://redmine.postgresql.org/issues/6081>`_ - Added support for advanced table fields like the foreign key, primary key in the ERD tool.
| `Issue #6529 <https://redmine.postgresql.org/issues/6529>`_ - Added index creation when generating SQL in the ERD tool.
| `Issue #6657 <https://redmine.postgresql.org/issues/6657>`_ - Added support for authentication via the webserver (REMOTE_USER).
Housekeeping
************
@ -20,6 +21,7 @@ Bug fixes
*********
| `Issue #6754 <https://redmine.postgresql.org/issues/6754>`_ - Ensure that query highlighting color in the query tool should be less intensive.
| `Issue #6719 <https://redmine.postgresql.org/issues/6719>`_ - Fixed OAuth2 integration redirect issue.
| `Issue #6797 <https://redmine.postgresql.org/issues/6797>`_ - Remove an extra blank line at the start of the SQL for function, procedure, and trigger function.
| `Issue #6828 <https://redmine.postgresql.org/issues/6828>`_ - Fixed an issue where the tree is not scrolling to the object selected from the search result.
| `Issue #6882 <https://redmine.postgresql.org/issues/6882>`_ - Ensure that columns should be displayed in the order of creation instead of alphabetical order in the browser tree.

44
docs/en_US/webserver.rst Normal file
View File

@ -0,0 +1,44 @@
.. _webserver:
********************************************
`Enabling Webserver Authentication`:index:
********************************************
To configure Webserver authentication, you must setup your webserver
with any authentication plug-in (such as Shibboleth, HTTP BASIC auth)
as long as it sets the REMOTE_USER environment variable.
To enable Webserver authentication for pgAdmin, you must configure the Webserver
settings in the *config_local.py* or *config_system.py* file (see the
:ref:`config.py <config_py>` documentation) on the system where pgAdmin is
installed in Server mode. You can copy these settings from *config.py* file
and modify the values for the following parameters:
.. csv-table::
:header: "**Parameter**", "**Description**"
:class: longtable
:widths: 35, 55
"AUTHENTICATION_SOURCES", "The default value for this parameter is *internal*.
To enable OAUTH2 authentication, you must include *webserver* in the list of values
for this parameter. you can modify the value as follows:
* [webserver]: pgAdmin will use only Webserver authentication.
* [webserver, internal]: pgAdmin will first try to authenticate the user
through webserver. If that authentication fails, then it will return back
to the login dialog where you need to provide internal pgAdmin user
credentials for authentication."
"WEBSERVER_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically
create a pgAdmin user corresponding to a successfully authenticated Webserver user.
Please note that password is not stored in the pgAdmin database."
Master Password
===============
In the multi user mode, pgAdmin uses user's login password to encrypt/decrypt the PostgreSQL server password.
In the Webserver authentication, the pgAdmin does not store the user's password, so we need an encryption key to store
the PostgreSQL server password.
To accomplish this, set the configuration parameter MASTER_PASSWORD to *True*, so upon setting the master password,
it will be used as an encryption key while storing the password. If it is False, the server password can not be stored.

View File

@ -570,7 +570,8 @@ ENHANCED_COOKIE_PROTECTION = True
# Default setting is internal
# External Supported Sources: ldap, kerberos, oauth2
# Multiple authentication can be achieved by setting this parameter to
# ['ldap', 'internal'] or ['oauth2', 'internal'] etc.
# ['ldap', 'internal'] or ['oauth2', 'internal'] or
# ['webserver', 'internal'] etc.
# pgAdmin will authenticate the user with ldap/oauth2 whatever first in the
# list, in case of failure the second authentication option will be considered.
@ -729,6 +730,12 @@ OAUTH2_CONFIG = [
OAUTH2_AUTO_CREATE_USER = True
##########################################################################
# Webserver Configuration
##########################################################################
WEBSERVER_AUTO_CREATE_USER = True
##########################################################################
# PSQL tool settings
##########################################################################

View File

@ -13,6 +13,8 @@ import sys
if sys.version_info < (3, 4):
raise Exception('This application must be run under Python 3.4 or later.')
os.environ['SCRIPT_NAME'] = '/pgadmin4'
import builtins
root = os.path.dirname(os.path.realpath(__file__))

View File

@ -46,7 +46,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, OAUTH2, INTERNAL, LDAP
from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP, WEBSERVER
# 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
@ -470,11 +470,17 @@ def create_app(app_name=None):
'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS
}))
if 'SCRIPT_NAME' in os.environ and os.environ["SCRIPT_NAME"]:
app.config.update(dict({
'APPLICATION_ROOT': os.environ["SCRIPT_NAME"]
}))
app.config.update(dict({
'INTERNAL': INTERNAL,
'LDAP': LDAP,
'KERBEROS': KERBEROS,
'OAUTH2': OAUTH2
'OAUTH2': OAUTH2,
'WEBSERVER': WEBSERVER
}))
security.init_app(app, user_datastore)
@ -771,15 +777,14 @@ def create_app(app_name=None):
elif config.SERVER_MODE and \
not current_user.is_authenticated and \
request.endpoint in ('redirects.index', 'security.login'):
if app.PGADMIN_EXTERNAL_AUTH_SOURCE == KERBEROS:
if app.PGADMIN_EXTERNAL_AUTH_SOURCE in [KERBEROS, WEBSERVER]:
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 app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
OAUTH2 and\
app.PGADMIN_EXTERNAL_AUTH_SOURCE not in [
KERBEROS, OAUTH2, WEBSERVER] and \
current_app.keyManager.get() is None and \
request.endpoint not in ('security.login', 'security.logout'):
logout_user()

View File

@ -0,0 +1,119 @@
##########################################################################
#
# 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 Webserver authentication."""
import random
import string
import config
from flask import request, current_app, session, Response, render_template, \
url_for
from flask_babelex import gettext
from flask_security import login_user
from .internal import BaseAuthentication
from pgadmin.model import User
from pgadmin.tools.user_management import create_user
from pgadmin.utils.constants import WEBSERVER
from pgadmin.utils import PgAdminModule
from pgadmin.utils.csrf import pgCSRFProtect
from flask_security.utils import logout_user
from os import environ, path, remove
class WebserverModule(PgAdminModule):
def register(self, app, options, first_registration=False):
# Do not look for the sub_modules,
# instead call blueprint.register(...) directly
super(PgAdminModule, self).register(app, options, first_registration)
def get_exposed_url_endpoints(self):
return ['webserver.login',
'webserver.logout']
def init_app(app):
MODULE_NAME = 'webserver'
blueprint = WebserverModule(MODULE_NAME, __name__, static_url_path='')
@blueprint.route("/login",
endpoint="login", methods=["GET"])
@pgCSRFProtect.exempt
def webserver_login():
logout_user()
return Response(render_template("browser/kerberos_login.html",
login_url=url_for('security.login'),
))
@blueprint.route("/logout",
endpoint="logout", methods=["GET"])
@pgCSRFProtect.exempt
def webserver_logout():
logout_user()
return Response(render_template("browser/kerberos_logout.html",
login_url=url_for('security.login'),
))
app.register_blueprint(blueprint)
class WebserverAuthentication(BaseAuthentication):
LOGIN_VIEW = 'webserver.login'
LOGOUT_VIEW = 'webserver.logout'
def get_source_name(self):
return WEBSERVER
def get_friendly_name(self):
return gettext("webserver")
def validate(self, form):
return True
def get_user(self):
return request.environ.get('REMOTE_USER')
def authenticate(self, form):
username = self.get_user()
if not username:
return False, gettext(
"Webserver authenticate failed.")
session['pass_enc_key'] = ''.join(
(random.choice(string.ascii_lowercase) for x in range(10)))
useremail = request.environ.get('mail')
if not useremail:
useremail = ''
return self.__auto_create_user(username, '')
def login(self, form):
username = self.get_user()
if username:
user = User.query.filter_by(username=username).first()
status = login_user(user)
if not status:
current_app.logger.exception(self.messages('LOGIN_FAILED'))
return False, self.messages('LOGIN_FAILED')
return True, None
return False, self.messages('LOGIN_FAILED')
def __auto_create_user(self, username, useremail):
"""Add the webserver user to the internal SQLite database."""
if config.WEBSERVER_AUTO_CREATE_USER:
user = User.query.filter_by(username=username).first()
if not user:
return create_user({
'username': username,
'email': useremail,
'role': 2,
'active': True,
'auth_source': WEBSERVER
})
return True, None

View File

@ -133,4 +133,5 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
"""
cls.tester.logout()
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
app_config.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
utils.login_tester_account(cls.tester)

View File

@ -95,4 +95,5 @@ class LDAPLoginTestCase(BaseTestGenerator):
"""
cls.tester.logout()
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
app_config.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
utils.login_tester_account(cls.tester)

View File

@ -81,4 +81,5 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
"""
cls.tester.logout()
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
app_config.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
utils.login_tester_account(cls.tester)

View File

@ -100,6 +100,7 @@ class LoginTestCase(BaseTestGenerator):
# No need to call base class setup function
def setUp(self):
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
app_config.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
def runTest(self):
"""This function checks login functionality."""

View File

@ -145,4 +145,5 @@ class Oauth2LoginMockTestCase(BaseTestGenerator):
"""
cls.tester.logout()
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
app_config.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
utils.login_tester_account(cls.tester)

View File

@ -0,0 +1,86 @@
##########################################################################
#
# 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
from pgadmin.authenticate import AuthSourceManager
from pgadmin.utils.constants import OAUTH2, LDAP, INTERNAL, WEBSERVER
from flask import request
class WebserverLoginMockTestCase(BaseTestGenerator):
"""
This class checks oauth2 login functionality by mocking
Webserver Authentication.
"""
scenarios = [
('Webserver Authentication', dict(
auth_source=[WEBSERVER],
username='test_mock_webserver_user'
)),
]
@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 = WEBSERVER
def runTest(self):
"""This function checks webserver login functionality."""
if app_config.SERVER_MODE is False:
self.skipTest(
"Can not run Webserver Authentication in the Desktop mode."
)
self.test_webserver_authentication()
def test_webserver_authentication(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.
"""
# Mock Oauth2 Authenticate
AuthSourceRegistry._registry[WEBSERVER].get_user = MagicMock(
return_value=self.username)
res = self.tester.login(None,
None,
True,
None
)
self.assertEqual(res.status_code, 200)
respdata = 'Gravatar image for %s' % self.username
self.assertTrue(respdata in res.data.decode('utf8'))
def tearDown(self):
pass
@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]
app_config.PGADMIN_EXTERNAL_AUTH_SOURCE = INTERNAL
utils.login_tester_account(cls.tester)

View File

@ -55,12 +55,14 @@ ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
INTERNAL = 'internal'
LDAP = 'ldap'
KERBEROS = 'kerberos'
OAUTH2 = "oauth2"
OAUTH2 = 'oauth2'
WEBSERVER = 'webserver'
SUPPORTED_AUTH_SOURCES = [INTERNAL,
LDAP,
KERBEROS,
OAUTH2]
OAUTH2,
WEBSERVER]
BINARY_PATHS = {
"as_bin_paths": [