mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-12-25 16:31:07 -06:00
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:
parent
ca40add29b
commit
a726635290
@ -39,6 +39,7 @@ Mode is pre-configured for security.
|
||||
ldap
|
||||
kerberos
|
||||
oauth2
|
||||
webserver
|
||||
|
||||
|
||||
.. note:: Pre-compiled and configured installation packages are available for
|
||||
|
@ -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
44
docs/en_US/webserver.rst
Normal 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.
|
@ -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
|
||||
##########################################################################
|
||||
|
@ -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__))
|
||||
|
@ -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()
|
||||
|
119
web/pgadmin/authenticate/webserver.py
Normal file
119
web/pgadmin/authenticate/webserver.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
86
web/pgadmin/browser/tests/test_webserver_with_mocking.py
Normal file
86
web/pgadmin/browser/tests/test_webserver_with_mocking.py
Normal 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)
|
@ -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": [
|
||||
|
Loading…
Reference in New Issue
Block a user