mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added support for OAuth 2 authentication. Fixes #5940
Initial patch sent by: Florian Sabonchi
This commit is contained in:
parent
fff4060b31
commit
48ca83f31d
@ -56,6 +56,7 @@ eventlet 0.31.0
|
|||||||
httpagentparser 1.9.1 http://www.opensource.org/licenses/mit-license.php http://shon.github.com/httpagentparser
|
httpagentparser 1.9.1 http://www.opensource.org/licenses/mit-license.php http://shon.github.com/httpagentparser
|
||||||
user-agents 2.2.0 MIT https://github.com/selwin/python-user-agents
|
user-agents 2.2.0 MIT https://github.com/selwin/python-user-agents
|
||||||
pywinpty 1.1.1 Unknown Unknown
|
pywinpty 1.1.1 Unknown Unknown
|
||||||
|
authlib 0.15.3 BSD https://github.com/lepture/authlib
|
||||||
|
|
||||||
NOTE: This report was generated using Python 3.9. Full information may not be
|
NOTE: This report was generated using Python 3.9. Full information may not be
|
||||||
shown for Python modules that are not required with this version.
|
shown for Python modules that are not required with this version.
|
||||||
|
@ -37,6 +37,7 @@ Mode is pre-configured for security.
|
|||||||
change_user_password
|
change_user_password
|
||||||
ldap
|
ldap
|
||||||
kerberos
|
kerberos
|
||||||
|
oauth2
|
||||||
|
|
||||||
|
|
||||||
.. note:: Pre-compiled and configured installation packages are available for
|
.. note:: Pre-compiled and configured installation packages are available for
|
||||||
|
BIN
docs/en_US/images/oauth2_login.png
Normal file
BIN
docs/en_US/images/oauth2_login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
@ -34,6 +34,7 @@ from *config.py* file and modify the values for the following parameters.
|
|||||||
Please note that if it is not set, it will take the value of
|
Please note that if it is not set, it will take the value of
|
||||||
*default_server* parameter."
|
*default_server* parameter."
|
||||||
|
|
||||||
|
|
||||||
Keytab file for HTTP Service
|
Keytab file for HTTP Service
|
||||||
============================
|
============================
|
||||||
|
|
||||||
@ -116,3 +117,13 @@ PostgreSQL Server settings to configure Kerberos Authentication
|
|||||||
|
|
||||||
* Note that, you have to login into pgAdmin with Kerberos authentication to
|
* Note that, you have to login into pgAdmin with Kerberos authentication to
|
||||||
then connect to PostgreSQL using Kerberos.
|
then connect to PostgreSQL using Kerberos.
|
||||||
|
|
||||||
|
|
||||||
|
Master Password
|
||||||
|
===============
|
||||||
|
|
||||||
|
In the multi user mode, pgAdmin uses user's login password to encrypt/decrypt the PostgreSQL server password.
|
||||||
|
In the Kerberos authentication, the pgAdmin user does not have the password, so we need an encryption key to store
|
||||||
|
the PostgreSQL server password for the servers which are not configured to use the Kerberos authentication.
|
||||||
|
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.
|
||||||
|
61
docs/en_US/oauth2.rst
Normal file
61
docs/en_US/oauth2.rst
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
.. _oauth2:
|
||||||
|
|
||||||
|
*****************************************
|
||||||
|
`Enabling OAUTH2 Authentication`:index:
|
||||||
|
*****************************************
|
||||||
|
|
||||||
|
|
||||||
|
To enable OAUTH2 authentication for pgAdmin, you must configure the OAUTH2
|
||||||
|
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 *oauth2* in the list of values
|
||||||
|
for this parameter. you can modify the value as follows:
|
||||||
|
|
||||||
|
* [‘oauth2’, ‘internal’]: pgAdmin will display an additional button for authenticating with oauth2"
|
||||||
|
"OAUTH2_NAME", "The name of the Oauth2 provider, ex: Google, Github"
|
||||||
|
"OAUTH2_DISPLAY_NAME", "Oauth2 display name in pgAdmin"
|
||||||
|
"OAUTH2_CLIENT_ID", "Oauth2 Client ID"
|
||||||
|
"OAUTH2_CLIENT_SECRET", "Oauth2 Client Secret"
|
||||||
|
"OAUTH2_TOKEN_URL", "Oauth2 Access Token endpoint"
|
||||||
|
"OAUTH2_AUTHORIZATION_URL", "Endpoint for user authorization"
|
||||||
|
"OAUTH2_API_BASE_URL", "Oauth2 base URL endpoint to make requests simple, ex: *https://api.github.com/*"
|
||||||
|
"OAUTH2_USERINFO_ENDPOINT", "User Endpoint, ex: *user* (for github) and *useinfo* (for google)"
|
||||||
|
"OAUTH2_ICON", "The Font-awesome icon to be placed on the oauth2 button, ex: fa-github"
|
||||||
|
"OAUTH2_BUTTON_COLOR", "Oauth2 button color"
|
||||||
|
"OAUTH2_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically
|
||||||
|
create a pgAdmin user corresponding to a successfully authenticated Oauth2 user.
|
||||||
|
Please note that password is not stored in the pgAdmin database."
|
||||||
|
|
||||||
|
Redirect URL
|
||||||
|
============
|
||||||
|
|
||||||
|
The redirect url to configure Oauth2 server is *http://<pgAdmin Server URL>/oauth2/authorize*
|
||||||
|
|
||||||
|
Master Password
|
||||||
|
===============
|
||||||
|
|
||||||
|
In the multi user mode, pgAdmin uses user's login password to encrypt/decrypt the PostgreSQL server password.
|
||||||
|
In the Oauth2 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.
|
||||||
|
|
||||||
|
|
||||||
|
Login Page
|
||||||
|
============
|
||||||
|
|
||||||
|
After configuration, on restart, you can see the login page with the Oauth2 login button(s).
|
||||||
|
|
||||||
|
.. image:: images/oauth2_login.png
|
||||||
|
:alt: Oauth2 login
|
||||||
|
:align: center
|
@ -12,6 +12,7 @@ New features
|
|||||||
| `Issue #1975 <https://redmine.postgresql.org/issues/1975>`_ - Highlighted long running queries on the dashboards.
|
| `Issue #1975 <https://redmine.postgresql.org/issues/1975>`_ - Highlighted long running queries on the dashboards.
|
||||||
| `Issue #3893 <https://redmine.postgresql.org/issues/3893>`_ - Added support for Reassign/Drop Owned for login roles.
|
| `Issue #3893 <https://redmine.postgresql.org/issues/3893>`_ - Added support for Reassign/Drop Owned for login roles.
|
||||||
| `Issue #3920 <https://redmine.postgresql.org/issues/3920>`_ - Do not block the query editor window when running a query.
|
| `Issue #3920 <https://redmine.postgresql.org/issues/3920>`_ - Do not block the query editor window when running a query.
|
||||||
|
| `Issue #5940 <https://redmine.postgresql.org/issues/5940>`_ - Added support for OAuth 2 authentication.
|
||||||
| `Issue #6559 <https://redmine.postgresql.org/issues/6559>`_ - Added option to provide maximum width of the column when 'Resize by data?’ option in the preferences is set to True.
|
| `Issue #6559 <https://redmine.postgresql.org/issues/6559>`_ - Added option to provide maximum width of the column when 'Resize by data?’ option in the preferences is set to True.
|
||||||
|
|
||||||
Housekeeping
|
Housekeeping
|
||||||
|
@ -41,3 +41,5 @@ eventlet==0.31.0
|
|||||||
httpagentparser==1.9.*
|
httpagentparser==1.9.*
|
||||||
user-agents==2.2.0
|
user-agents==2.2.0
|
||||||
pywinpty==1.1.1; sys_platform=="win32"
|
pywinpty==1.1.1; sys_platform=="win32"
|
||||||
|
Authlib==0.15.*
|
||||||
|
requests==2.25.*
|
||||||
|
@ -562,10 +562,11 @@ ENHANCED_COOKIE_PROTECTION = True
|
|||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# Default setting is internal
|
# Default setting is internal
|
||||||
# External Supported Sources: ldap, kerberos
|
# External Supported Sources: ldap, kerberos, oauth2
|
||||||
# Multiple authentication can be achieved by setting this parameter to
|
# Multiple authentication can be achieved by setting this parameter to
|
||||||
# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first,
|
# ['ldap', 'internal'] or ['oauth2', 'internal'] etc.
|
||||||
# in case of failure internal authentication will be done.
|
# pgAdmin will authenticate the user with ldap/oauth2 whatever first in the
|
||||||
|
# list, in case of failure the second authentication option will be considered.
|
||||||
|
|
||||||
AUTHENTICATION_SOURCES = ['internal']
|
AUTHENTICATION_SOURCES = ['internal']
|
||||||
|
|
||||||
@ -666,6 +667,47 @@ KRB_AUTO_CREATE_USER = True
|
|||||||
|
|
||||||
KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
|
KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OAuth2 Configuration
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
# Multiple OAUTH2 providers can be added in the list like [{...},{...}]
|
||||||
|
# All parameters are required
|
||||||
|
|
||||||
|
OAUTH2_CONFIG = [
|
||||||
|
{
|
||||||
|
# The name of the of the oauth provider, ex: github, google
|
||||||
|
'OAUTH2_NAME': None,
|
||||||
|
# The display name, ex: Google
|
||||||
|
'OAUTH2_DISPLAY_NAME': '<Oauth2 Display Name>',
|
||||||
|
# Oauth client id
|
||||||
|
'OAUTH2_CLIENT_ID': None,
|
||||||
|
# Oauth secret
|
||||||
|
'OAUTH2_CLIENT_SECRET': None,
|
||||||
|
# URL to generate a token,
|
||||||
|
# Ex: https://github.com/login/oauth/access_token
|
||||||
|
'OAUTH2_TOKEN_URL': None,
|
||||||
|
# URL is used for authentication,
|
||||||
|
# Ex: https://github.com/login/oauth/authorize
|
||||||
|
'OAUTH2_AUTHORIZATION_URL': None,
|
||||||
|
# Oauth base url, ex: https://api.github.com/
|
||||||
|
'OAUTH2_API_BASE_URL': None,
|
||||||
|
# Name of the Endpoint, ex: user
|
||||||
|
'OAUTH2_USERINFO_ENDPOINT': None,
|
||||||
|
# Font-awesome icon, ex: fa-github
|
||||||
|
'OAUTH2_ICON': None,
|
||||||
|
# UI button colour, ex: #0000ff
|
||||||
|
'OAUTH2_BUTTON_COLOR': None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# After Oauth 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.
|
||||||
|
|
||||||
|
OAUTH2_AUTO_CREATE_USER = True
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# PSQL tool settings
|
# PSQL tool settings
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -46,12 +46,13 @@ from pgadmin.utils.ajax import internal_server_error, make_json_response
|
|||||||
from pgadmin.utils.csrf import pgCSRFProtect
|
from pgadmin.utils.csrf import pgCSRFProtect
|
||||||
from pgadmin import authenticate
|
from pgadmin import authenticate
|
||||||
from pgadmin.utils.security_headers import SecurityHeaders
|
from pgadmin.utils.security_headers import SecurityHeaders
|
||||||
from pgadmin.utils.constants import KERBEROS
|
from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP
|
||||||
|
|
||||||
# Explicitly set the mime-types so that a corrupted windows registry will not
|
# 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
|
# affect pgAdmin 4 to be load properly. This will avoid the issues that may
|
||||||
# occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".
|
# occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
mimetypes.add_type('application/javascript', '.js')
|
mimetypes.add_type('application/javascript', '.js')
|
||||||
mimetypes.add_type('text/css', '.css')
|
mimetypes.add_type('text/css', '.css')
|
||||||
|
|
||||||
@ -469,6 +470,13 @@ def create_app(app_name=None):
|
|||||||
'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS
|
'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
app.config.update(dict({
|
||||||
|
'INTERNAL': INTERNAL,
|
||||||
|
'LDAP': LDAP,
|
||||||
|
'KERBEROS': KERBEROS,
|
||||||
|
'OAUTH2': OAUTH2
|
||||||
|
}))
|
||||||
|
|
||||||
security.init_app(app, user_datastore)
|
security.init_app(app, user_datastore)
|
||||||
|
|
||||||
# register custom unauthorised handler.
|
# register custom unauthorised handler.
|
||||||
@ -760,19 +768,18 @@ def create_app(app_name=None):
|
|||||||
)
|
)
|
||||||
abort(401)
|
abort(401)
|
||||||
login_user(user)
|
login_user(user)
|
||||||
elif config.SERVER_MODE and\
|
elif config.SERVER_MODE and \
|
||||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\
|
|
||||||
KERBEROS and \
|
|
||||||
not current_user.is_authenticated and \
|
not current_user.is_authenticated and \
|
||||||
request.endpoint in ('redirects.index', 'security.login'):
|
request.endpoint in ('redirects.index', 'security.login'):
|
||||||
return authenticate.login()
|
if app.PGADMIN_EXTERNAL_AUTH_SOURCE == KERBEROS:
|
||||||
|
return authenticate.login()
|
||||||
# if the server is restarted the in memory key will be lost
|
# if the server is restarted the in memory key will be lost
|
||||||
# but the user session may still be active. Logout the user
|
# but the user session may still be active. Logout the user
|
||||||
# to get the key again when login
|
# to get the key again when login
|
||||||
if config.SERVER_MODE and current_user.is_authenticated and \
|
if config.SERVER_MODE and current_user.is_authenticated and \
|
||||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
|
app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
|
||||||
KERBEROS and \
|
KERBEROS and app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
|
||||||
|
OAUTH2 and\
|
||||||
current_app.keyManager.get() is None and \
|
current_app.keyManager.get() is None and \
|
||||||
request.endpoint not in ('security.login', 'security.logout'):
|
request.endpoint not in ('security.login', 'security.logout'):
|
||||||
logout_user()
|
logout_user()
|
||||||
|
@ -9,68 +9,33 @@
|
|||||||
|
|
||||||
"""A blueprint module implementing the Authentication."""
|
"""A blueprint module implementing the Authentication."""
|
||||||
|
|
||||||
import flask
|
|
||||||
import pickle
|
|
||||||
from flask import current_app, flash, Response, request, url_for,\
|
|
||||||
render_template
|
|
||||||
from flask_babelex import gettext
|
|
||||||
from flask_security import current_user, login_required
|
|
||||||
from flask_security.views import _security, _ctx
|
|
||||||
from flask_security.utils import config_value, get_post_logout_redirect, \
|
|
||||||
get_post_login_redirect, logout_user
|
|
||||||
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import session
|
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from pgadmin.utils import PgAdminModule
|
import copy
|
||||||
from pgadmin.utils.constants import KERBEROS, INTERNAL
|
|
||||||
from pgadmin.utils.csrf import pgCSRFProtect
|
from flask import current_app, flash, Response, request, url_for,\
|
||||||
|
session, redirect
|
||||||
|
from flask_babelex import gettext
|
||||||
|
from flask_security.views import _security
|
||||||
|
from flask_security.utils import get_post_logout_redirect, \
|
||||||
|
get_post_login_redirect
|
||||||
|
|
||||||
|
from pgadmin.utils import PgAdminModule
|
||||||
|
from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP
|
||||||
|
from pgadmin.authenticate.registry import AuthSourceRegistry
|
||||||
|
|
||||||
from .registry import AuthSourceRegistry
|
|
||||||
|
|
||||||
MODULE_NAME = 'authenticate'
|
MODULE_NAME = 'authenticate'
|
||||||
|
auth_obj = None
|
||||||
|
|
||||||
|
|
||||||
class AuthenticateModule(PgAdminModule):
|
class AuthenticateModule(PgAdminModule):
|
||||||
def get_exposed_url_endpoints(self):
|
def get_exposed_url_endpoints(self):
|
||||||
return ['authenticate.login',
|
return ['authenticate.login']
|
||||||
'authenticate.kerberos_login',
|
|
||||||
'authenticate.kerberos_logout',
|
|
||||||
'authenticate.kerberos_update_ticket',
|
|
||||||
'authenticate.kerberos_validate_ticket']
|
|
||||||
|
|
||||||
|
|
||||||
blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
|
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()
|
|
||||||
if 'KRB5CCNAME' in session:
|
|
||||||
# Remove the credential cache
|
|
||||||
cache_file_path = session['KRB5CCNAME'].split(":")[1]
|
|
||||||
if os.path.exists(cache_file_path):
|
|
||||||
os.remove(cache_file_path)
|
|
||||||
|
|
||||||
return Response(render_template("browser/kerberos_logout.html",
|
|
||||||
login_url=url_for('security.login'),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
|
@blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
"""
|
"""
|
||||||
@ -78,15 +43,20 @@ def login():
|
|||||||
The user input will be validated and authenticated.
|
The user input will be validated and authenticated.
|
||||||
"""
|
"""
|
||||||
form = _security.login_form()
|
form = _security.login_form()
|
||||||
auth_obj = AuthSourceManager(form, config.AUTHENTICATION_SOURCES)
|
|
||||||
session['_auth_source_manager_obj'] = None
|
|
||||||
|
|
||||||
|
auth_obj = AuthSourceManager(form, copy.deepcopy(
|
||||||
|
config.AUTHENTICATION_SOURCES))
|
||||||
|
if OAUTH2 in config.AUTHENTICATION_SOURCES\
|
||||||
|
and 'oauth2_button' in request.form:
|
||||||
|
session['auth_obj'] = auth_obj
|
||||||
|
|
||||||
|
session['auth_source_manager'] = None
|
||||||
# Validate the user
|
# Validate the user
|
||||||
if not auth_obj.validate():
|
if not auth_obj.validate():
|
||||||
for field in form.errors:
|
for field in form.errors:
|
||||||
for error in form.errors[field]:
|
for error in form.errors[field]:
|
||||||
flash(error, 'warning')
|
flash(error, 'warning')
|
||||||
return flask.redirect(get_post_logout_redirect())
|
return redirect(get_post_logout_redirect())
|
||||||
|
|
||||||
# Authenticate the user
|
# Authenticate the user
|
||||||
status, msg = auth_obj.authenticate()
|
status, msg = auth_obj.authenticate()
|
||||||
@ -94,34 +64,40 @@ def login():
|
|||||||
# Login the user
|
# Login the user
|
||||||
status, msg = auth_obj.login()
|
status, msg = auth_obj.login()
|
||||||
current_auth_obj = auth_obj.as_dict()
|
current_auth_obj = auth_obj.as_dict()
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
if current_auth_obj['current_source'] ==\
|
if current_auth_obj['current_source'] ==\
|
||||||
KERBEROS:
|
KERBEROS:
|
||||||
return flask.redirect('{0}?next={1}'.format(url_for(
|
return redirect('{0}?next={1}'.format(url_for(
|
||||||
'authenticate.kerberos_login'), url_for('browser.index')))
|
'authenticate.kerberos_login'), url_for('browser.index')))
|
||||||
|
|
||||||
flash(msg, 'danger')
|
flash(msg, 'danger')
|
||||||
return flask.redirect(get_post_logout_redirect())
|
return redirect(get_post_logout_redirect())
|
||||||
|
session['auth_source_manager'] = current_auth_obj
|
||||||
session['_auth_source_manager_obj'] = current_auth_obj
|
if 'auth_obj' in session:
|
||||||
return flask.redirect(get_post_login_redirect())
|
session['auth_obj'] = None
|
||||||
|
return redirect(get_post_login_redirect())
|
||||||
|
|
||||||
elif isinstance(msg, Response):
|
elif isinstance(msg, Response):
|
||||||
return msg
|
return msg
|
||||||
|
elif 'oauth2_button' in request.form and not isinstance(msg, str):
|
||||||
|
return msg
|
||||||
flash(msg, 'danger')
|
flash(msg, 'danger')
|
||||||
response = flask.redirect(get_post_logout_redirect())
|
response = redirect(get_post_logout_redirect())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AuthSourceManager():
|
class AuthSourceManager:
|
||||||
"""This class will manage all the authentication sources.
|
"""This class will manage all the authentication sources.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, form, sources):
|
def __init__(self, form, sources):
|
||||||
self.form = form
|
self.form = form
|
||||||
self.auth_sources = sources
|
self.auth_sources = sources
|
||||||
self.source = None
|
self.source = None
|
||||||
self.source_friendly_name = INTERNAL
|
self.source_friendly_name = INTERNAL
|
||||||
self.current_source = None
|
self.current_source = INTERNAL
|
||||||
|
self.update_auth_sources()
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
"""
|
"""
|
||||||
@ -135,6 +111,14 @@ class AuthSourceManager():
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def update_auth_sources(self):
|
||||||
|
for auth_src in [KERBEROS, OAUTH2]:
|
||||||
|
if auth_src in self.auth_sources:
|
||||||
|
if 'internal_button' in request.form:
|
||||||
|
self.auth_sources.remove(auth_src)
|
||||||
|
elif INTERNAL in self.auth_sources:
|
||||||
|
self.auth_sources.remove(INTERNAL)
|
||||||
|
|
||||||
def set_current_source(self, source):
|
def set_current_source(self, source):
|
||||||
self.current_source = source
|
self.current_source = source
|
||||||
|
|
||||||
@ -170,36 +154,19 @@ class AuthSourceManager():
|
|||||||
msg = None
|
msg = None
|
||||||
for src in self.auth_sources:
|
for src in self.auth_sources:
|
||||||
source = get_auth_sources(src)
|
source = get_auth_sources(src)
|
||||||
|
self.set_source(source)
|
||||||
current_app.logger.debug(
|
current_app.logger.debug(
|
||||||
"Authentication initiated via source: %s" %
|
"Authentication initiated via source: %s" %
|
||||||
source.get_source_name())
|
source.get_source_name())
|
||||||
|
|
||||||
if self.form.data['email'] and self.form.data['password'] and \
|
|
||||||
source.get_source_name() == KERBEROS:
|
|
||||||
msg = gettext('pgAdmin internal user authentication'
|
|
||||||
' is not enabled, please contact administrator.')
|
|
||||||
continue
|
|
||||||
|
|
||||||
status, msg = source.authenticate(self.form)
|
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:
|
if status:
|
||||||
self.set_source(source)
|
|
||||||
self.set_current_source(source.get_source_name())
|
self.set_current_source(source.get_source_name())
|
||||||
if msg is not None and 'username' in msg:
|
if msg is not None and 'username' in msg:
|
||||||
self.form._fields['email'].data = msg['username']
|
self.form._fields['email'].data = msg['username']
|
||||||
return status, msg
|
return status, msg
|
||||||
|
|
||||||
return status, msg
|
return status, msg
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
@ -209,6 +176,13 @@ class AuthSourceManager():
|
|||||||
current_app.logger.debug(
|
current_app.logger.debug(
|
||||||
"Authentication and Login successfully done via source : %s" %
|
"Authentication and Login successfully done via source : %s" %
|
||||||
self.source.get_source_name())
|
self.source.get_source_name())
|
||||||
|
|
||||||
|
# Set the login, logout view as per source if available
|
||||||
|
current_app.login_manager.login_view = getattr(
|
||||||
|
self.source, 'LOGIN_VIEW', 'security.login')
|
||||||
|
current_app.login_manager.logout_view = getattr(
|
||||||
|
self.source, 'LOGOUT_VIEW', 'security.logout')
|
||||||
|
|
||||||
return status, msg
|
return status, msg
|
||||||
|
|
||||||
|
|
||||||
@ -239,58 +213,3 @@ def init_app(app):
|
|||||||
AuthSourceRegistry.load_modules(app)
|
AuthSourceRegistry.load_modules(app)
|
||||||
|
|
||||||
return auth_sources
|
return auth_sources
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/kerberos/update_ticket",
|
|
||||||
endpoint="kerberos_update_ticket", methods=["GET"])
|
|
||||||
@pgCSRFProtect.exempt
|
|
||||||
@login_required
|
|
||||||
def kerberos_update_ticket():
|
|
||||||
"""
|
|
||||||
Update the kerberos ticket.
|
|
||||||
"""
|
|
||||||
from werkzeug.datastructures import Headers
|
|
||||||
headers = Headers()
|
|
||||||
|
|
||||||
authorization = request.headers.get("Authorization", None)
|
|
||||||
|
|
||||||
if authorization is None:
|
|
||||||
# Send the Negotiate header to the client
|
|
||||||
# if Kerberos ticket is not found.
|
|
||||||
headers.add('WWW-Authenticate', 'Negotiate')
|
|
||||||
return Response("Unauthorised", 401, headers)
|
|
||||||
else:
|
|
||||||
source = get_auth_sources(KERBEROS)
|
|
||||||
auth_header = authorization.split()
|
|
||||||
in_token = auth_header[1]
|
|
||||||
|
|
||||||
# Validate the Kerberos ticket
|
|
||||||
status, context = source.negotiate_start(in_token)
|
|
||||||
if status:
|
|
||||||
return Response("Ticket updated successfully.")
|
|
||||||
|
|
||||||
return Response(context, 500)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/kerberos/validate_ticket",
|
|
||||||
endpoint="kerberos_validate_ticket", methods=["GET"])
|
|
||||||
@pgCSRFProtect.exempt
|
|
||||||
@login_required
|
|
||||||
def kerberos_validate_ticket():
|
|
||||||
"""
|
|
||||||
Return the kerberos ticket lifetime left after getting the
|
|
||||||
ticket from the credential cache
|
|
||||||
"""
|
|
||||||
import gssapi
|
|
||||||
|
|
||||||
try:
|
|
||||||
del_creds = gssapi.Credentials(store={'ccache': session['KRB5CCNAME']})
|
|
||||||
creds = del_creds.acquire(store={'ccache': session['KRB5CCNAME']})
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.exception(e)
|
|
||||||
return internal_server_error(errormsg=str(e))
|
|
||||||
|
|
||||||
return make_json_response(
|
|
||||||
data={'ticket_lifetime': creds.lifetime},
|
|
||||||
status=200
|
|
||||||
)
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"""Implements Internal Authentication"""
|
"""Implements Internal Authentication"""
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from flask import current_app
|
from flask import current_app, flash
|
||||||
from flask_security import login_user
|
from flask_security import login_user
|
||||||
from abc import abstractmethod, abstractproperty
|
from abc import abstractmethod, abstractproperty
|
||||||
from flask_babelex import gettext
|
from flask_babelex import gettext
|
||||||
@ -31,6 +31,8 @@ class BaseAuthentication(object):
|
|||||||
'PASSWORD_NOT_PROVIDED': gettext('Password not provided'),
|
'PASSWORD_NOT_PROVIDED': gettext('Password not provided'),
|
||||||
'INVALID_EMAIL': gettext('Email/Username is not valid')
|
'INVALID_EMAIL': gettext('Email/Username is not valid')
|
||||||
}
|
}
|
||||||
|
LOGIN_VIEW = 'security.login'
|
||||||
|
LOGOUT_VIEW = 'security.logout'
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_source_name(self):
|
def get_source_name(self):
|
||||||
@ -97,7 +99,7 @@ class InternalAuthentication(BaseAuthentication):
|
|||||||
"""User validation"""
|
"""User validation"""
|
||||||
# validate the email id first
|
# validate the email id first
|
||||||
if not validate_email(form.data['email']):
|
if not validate_email(form.data['email']):
|
||||||
form.errors['email'] = [self.messages('INVALID_EMAIL')]
|
flash(self.messages('INVALID_EMAIL'), 'warning')
|
||||||
return False
|
return False
|
||||||
# Flask security validation
|
# Flask security validation
|
||||||
return form.validate_on_submit()
|
return form.validate_on_submit()
|
||||||
|
@ -10,22 +10,28 @@
|
|||||||
"""A blueprint module implementing the Spnego/Kerberos authentication."""
|
"""A blueprint module implementing the Spnego/Kerberos authentication."""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from os import environ, path
|
from os import environ, path, remove
|
||||||
|
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers, MultiDict
|
||||||
from flask_babelex import gettext
|
from flask_babelex import gettext
|
||||||
from flask import Flask, request, Response, session,\
|
from flask import request, Response, session,\
|
||||||
current_app, render_template, flash
|
current_app, render_template, flash, url_for
|
||||||
|
from flask_security.views import _security
|
||||||
|
from flask_security.utils import logout_user
|
||||||
|
from flask_security import login_required
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from pgadmin.model import User
|
from pgadmin.model import User
|
||||||
from pgadmin.tools.user_management import create_user
|
from pgadmin.tools.user_management import create_user
|
||||||
from pgadmin.utils.constants import KERBEROS
|
from pgadmin.utils.constants import KERBEROS
|
||||||
|
from pgadmin.utils import PgAdminModule
|
||||||
|
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
||||||
|
|
||||||
from flask_security.views import _security
|
|
||||||
from werkzeug.datastructures import MultiDict
|
|
||||||
|
|
||||||
from .internal import BaseAuthentication
|
from pgadmin.authenticate.internal import BaseAuthentication
|
||||||
|
from pgadmin.authenticate import get_auth_sources
|
||||||
|
from pgadmin.utils.csrf import pgCSRFProtect
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import gssapi
|
import gssapi
|
||||||
@ -46,8 +52,110 @@ if config.KRB_KTNAME and config.KRB_KTNAME != '<KRB5_KEYTAB_FILE>':
|
|||||||
environ['KRB5_KTNAME'] = config.KRB_KTNAME
|
environ['KRB5_KTNAME'] = config.KRB_KTNAME
|
||||||
|
|
||||||
|
|
||||||
|
class KerberosModule(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 ['kerberos.login',
|
||||||
|
'kerberos.logout',
|
||||||
|
'kerberos.update_ticket',
|
||||||
|
'kerberos.validate_ticket']
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
MODULE_NAME = 'kerberos'
|
||||||
|
|
||||||
|
blueprint = KerberosModule(MODULE_NAME, __name__, static_url_path='')
|
||||||
|
|
||||||
|
@blueprint.route("/login",
|
||||||
|
endpoint="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",
|
||||||
|
endpoint="logout", methods=["GET"])
|
||||||
|
@pgCSRFProtect.exempt
|
||||||
|
def kerberos_logout():
|
||||||
|
logout_user()
|
||||||
|
if 'KRB5CCNAME' in session:
|
||||||
|
# Remove the credential cache
|
||||||
|
cache_file_path = session['KRB5CCNAME'].split(":")[1]
|
||||||
|
if path.exists(cache_file_path):
|
||||||
|
remove(cache_file_path)
|
||||||
|
|
||||||
|
return Response(render_template("browser/kerberos_logout.html",
|
||||||
|
login_url=url_for('security.login'),
|
||||||
|
))
|
||||||
|
|
||||||
|
@blueprint.route("/update_ticket",
|
||||||
|
endpoint="update_ticket", methods=["GET"])
|
||||||
|
@pgCSRFProtect.exempt
|
||||||
|
@login_required
|
||||||
|
def kerberos_update_ticket():
|
||||||
|
"""
|
||||||
|
Update the kerberos ticket.
|
||||||
|
"""
|
||||||
|
from werkzeug.datastructures import Headers
|
||||||
|
headers = Headers()
|
||||||
|
|
||||||
|
authorization = request.headers.get("Authorization", None)
|
||||||
|
|
||||||
|
if authorization is None:
|
||||||
|
# Send the Negotiate header to the client
|
||||||
|
# if Kerberos ticket is not found.
|
||||||
|
headers.add('WWW-Authenticate', 'Negotiate')
|
||||||
|
return Response("Unauthorised", 401, headers)
|
||||||
|
else:
|
||||||
|
source = get_auth_sources(KERBEROS)
|
||||||
|
auth_header = authorization.split()
|
||||||
|
in_token = auth_header[1]
|
||||||
|
|
||||||
|
# Validate the Kerberos ticket
|
||||||
|
status, context = source.negotiate_start(in_token)
|
||||||
|
if status:
|
||||||
|
return Response("Ticket updated successfully.")
|
||||||
|
|
||||||
|
return Response(context, 500)
|
||||||
|
|
||||||
|
@blueprint.route("/validate_ticket",
|
||||||
|
endpoint="validate_ticket", methods=["GET"])
|
||||||
|
@pgCSRFProtect.exempt
|
||||||
|
@login_required
|
||||||
|
def kerberos_validate_ticket():
|
||||||
|
"""
|
||||||
|
Return the kerberos ticket lifetime left after getting the
|
||||||
|
ticket from the credential cache
|
||||||
|
"""
|
||||||
|
import gssapi
|
||||||
|
|
||||||
|
try:
|
||||||
|
del_creds = gssapi.Credentials(store={
|
||||||
|
'ccache': session['KRB5CCNAME']})
|
||||||
|
creds = del_creds.acquire(store={'ccache': session['KRB5CCNAME']})
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.exception(e)
|
||||||
|
return internal_server_error(errormsg=str(e))
|
||||||
|
|
||||||
|
return make_json_response(
|
||||||
|
data={'ticket_lifetime': creds.lifetime},
|
||||||
|
status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
app.register_blueprint(blueprint)
|
||||||
|
|
||||||
|
|
||||||
class KerberosAuthentication(BaseAuthentication):
|
class KerberosAuthentication(BaseAuthentication):
|
||||||
|
|
||||||
|
LOGIN_VIEW = 'kerberos.login'
|
||||||
|
LOGOUT_VIEW = 'kerberos.logout'
|
||||||
|
|
||||||
def get_source_name(self):
|
def get_source_name(self):
|
||||||
return KERBEROS
|
return KERBEROS
|
||||||
|
|
||||||
@ -85,7 +193,7 @@ class KerberosAuthentication(BaseAuthentication):
|
|||||||
if status:
|
if status:
|
||||||
# Saving the first 15 characters of the kerberos key
|
# Saving the first 15 characters of the kerberos key
|
||||||
# to encrypt/decrypt database password
|
# to encrypt/decrypt database password
|
||||||
session['kerberos_key'] = auth_header[1][0:15]
|
session['pass_enc_key'] = auth_header[1][0:15]
|
||||||
# Create user
|
# Create user
|
||||||
retval = self.__auto_create_user(
|
retval = self.__auto_create_user(
|
||||||
str(negotiate.initiator_name))
|
str(negotiate.initiator_name))
|
||||||
@ -162,7 +270,7 @@ class KerberosAuthentication(BaseAuthentication):
|
|||||||
username = str(username)
|
username = str(username)
|
||||||
if config.KRB_AUTO_CREATE_USER:
|
if config.KRB_AUTO_CREATE_USER:
|
||||||
user = User.query.filter_by(
|
user = User.query.filter_by(
|
||||||
username=username).first()
|
username=username, auth_source=KERBEROS).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
return create_user({
|
return create_user({
|
||||||
'username': username,
|
'username': username,
|
||||||
|
172
web/pgadmin/authenticate/oauth2.py
Normal file
172
web/pgadmin/authenticate/oauth2.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
##########################################################################
|
||||||
|
#
|
||||||
|
# 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 Oauth2 authentication."""
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
from flask import current_app, url_for, session, request,\
|
||||||
|
redirect, Flask, flash
|
||||||
|
from flask_babelex import gettext
|
||||||
|
from flask_security import login_user, current_user
|
||||||
|
from flask_security.utils import get_post_logout_redirect, \
|
||||||
|
get_post_login_redirect, logout_user
|
||||||
|
|
||||||
|
from pgadmin.authenticate.internal import BaseAuthentication
|
||||||
|
from pgadmin.model import User
|
||||||
|
from pgadmin.tools.user_management import create_user
|
||||||
|
from pgadmin.utils.constants import OAUTH2
|
||||||
|
from pgadmin.utils import PgAdminModule
|
||||||
|
from pgadmin.utils.csrf import pgCSRFProtect
|
||||||
|
from pgadmin.model import db
|
||||||
|
|
||||||
|
OAUTH2_LOGOUT = 'oauth2.logout'
|
||||||
|
OAUTH2_AUTHORIZE = 'oauth2.authorize'
|
||||||
|
|
||||||
|
|
||||||
|
class Oauth2Module(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 [OAUTH2_AUTHORIZE,
|
||||||
|
OAUTH2_LOGOUT]
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
MODULE_NAME = 'oauth2'
|
||||||
|
|
||||||
|
blueprint = Oauth2Module(MODULE_NAME, __name__, static_url_path='')
|
||||||
|
|
||||||
|
@blueprint.route('/authorize', endpoint="authorize",
|
||||||
|
methods=['GET', 'POST'])
|
||||||
|
@pgCSRFProtect.exempt
|
||||||
|
def oauth_authorize():
|
||||||
|
auth_obj = session['auth_obj']
|
||||||
|
auth_obj.set_current_source(auth_obj.source.get_source_name())
|
||||||
|
status, msg = auth_obj.login()
|
||||||
|
if status:
|
||||||
|
session['auth_source_manager'] = auth_obj.as_dict()
|
||||||
|
session['auth_obj'] = None
|
||||||
|
return redirect(get_post_login_redirect())
|
||||||
|
logout_user()
|
||||||
|
flash(msg)
|
||||||
|
return redirect(get_post_login_redirect())
|
||||||
|
|
||||||
|
@blueprint.route('/logout', endpoint="logout",
|
||||||
|
methods=['GET', 'POST'])
|
||||||
|
@pgCSRFProtect.exempt
|
||||||
|
def oauth_logout():
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return redirect(get_post_logout_redirect())
|
||||||
|
for key in list(session.keys()):
|
||||||
|
session.pop(key)
|
||||||
|
logout_user()
|
||||||
|
return redirect(get_post_logout_redirect())
|
||||||
|
|
||||||
|
app.register_blueprint(blueprint)
|
||||||
|
app.login_manager.logout_view = OAUTH2_LOGOUT
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Authentication(BaseAuthentication):
|
||||||
|
"""OAuth Authentication Class"""
|
||||||
|
|
||||||
|
LOGOUT_VIEW = OAUTH2_LOGOUT
|
||||||
|
|
||||||
|
oauth_obj = OAuth(Flask(__name__))
|
||||||
|
oauth2_clients = {}
|
||||||
|
oauth2_config = {}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
for oauth2_config in config.OAUTH2_CONFIG:
|
||||||
|
|
||||||
|
OAuth2Authentication.oauth2_config[
|
||||||
|
oauth2_config['OAUTH2_NAME']] = oauth2_config
|
||||||
|
|
||||||
|
OAuth2Authentication.oauth2_clients[
|
||||||
|
oauth2_config['OAUTH2_NAME']
|
||||||
|
] = OAuth2Authentication.oauth_obj.register(
|
||||||
|
name=oauth2_config['OAUTH2_NAME'],
|
||||||
|
client_id=oauth2_config['OAUTH2_CLIENT_ID'],
|
||||||
|
client_secret=oauth2_config['OAUTH2_CLIENT_SECRET'],
|
||||||
|
access_token_url=oauth2_config['OAUTH2_TOKEN_URL'],
|
||||||
|
authorize_url=oauth2_config['OAUTH2_AUTHORIZATION_URL'],
|
||||||
|
api_base_url=oauth2_config['OAUTH2_API_BASE_URL'],
|
||||||
|
client_kwargs={'scope': 'email profile'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_source_name(self):
|
||||||
|
return OAUTH2
|
||||||
|
|
||||||
|
def get_friendly_name(self):
|
||||||
|
return self.oauth2_config[self.oauth2_current_client]['OAUTH2_NAME']
|
||||||
|
|
||||||
|
def validate(self, form):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def login(self, form):
|
||||||
|
profile = self.get_user_profile()
|
||||||
|
print(profile)
|
||||||
|
if 'email' not in profile or not profile['email']:
|
||||||
|
current_app.logger.exception(
|
||||||
|
'An email is required for authentication'
|
||||||
|
)
|
||||||
|
return False, gettext(
|
||||||
|
"An email is required for the oauth authentication.")
|
||||||
|
|
||||||
|
user, msg = self.__auto_create_user(profile)
|
||||||
|
if user:
|
||||||
|
user = db.session.query(User).filter_by(
|
||||||
|
username=profile['email'], auth_source=OAUTH2).first()
|
||||||
|
current_app.login_manager.logout_view = \
|
||||||
|
OAuth2Authentication.LOGOUT_VIEW
|
||||||
|
return login_user(user), None
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
def get_user_profile(self):
|
||||||
|
session['oauth2_token'] = self.oauth2_clients[
|
||||||
|
self.oauth2_current_client].authorize_access_token()
|
||||||
|
|
||||||
|
session['pass_enc_key'] = session['oauth2_token']['access_token']
|
||||||
|
|
||||||
|
resp = self.oauth2_clients[self.oauth2_current_client].get(
|
||||||
|
self.oauth2_config[
|
||||||
|
self.oauth2_current_client]['OAUTH2_USERINFO_ENDPOINT'],
|
||||||
|
token=session['oauth2_token']
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def authenticate(self, form):
|
||||||
|
self.oauth2_current_client = request.form['oauth2_button']
|
||||||
|
redirect_url = url_for(OAUTH2_AUTHORIZE, _external=True)
|
||||||
|
|
||||||
|
if self.oauth2_current_client not in self.oauth2_clients:
|
||||||
|
return False, gettext(
|
||||||
|
"Please set the configuration parameters properly.")
|
||||||
|
return False, self.oauth2_clients[
|
||||||
|
self.oauth2_current_client].authorize_redirect(redirect_url)
|
||||||
|
|
||||||
|
def __auto_create_user(self, resp):
|
||||||
|
if config.OAUTH2_AUTO_CREATE_USER:
|
||||||
|
user = User.query.filter_by(username=resp['email'],
|
||||||
|
auth_source=OAUTH2).first()
|
||||||
|
if not user:
|
||||||
|
return create_user({
|
||||||
|
'username': resp['email'],
|
||||||
|
'email': resp['email'],
|
||||||
|
'role': 2,
|
||||||
|
'active': True,
|
||||||
|
'auth_source': OAUTH2
|
||||||
|
})
|
||||||
|
|
||||||
|
return True, {'username': resp['email']}
|
@ -1,10 +1,19 @@
|
|||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// pgAdmin 4 - PostgreSQL Tools
|
||||||
|
//
|
||||||
|
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||||
|
// This software is released under the PostgreSQL Licence
|
||||||
|
//
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import url_for from 'sources/url_for';
|
import url_for from 'sources/url_for';
|
||||||
import userInfo from 'pgadmin.user_management.current_user';
|
import userInfo from 'pgadmin.user_management.current_user';
|
||||||
import pgConst from 'pgadmin.browser.constants';
|
import pgConst from 'pgadmin.browser.constants';
|
||||||
|
|
||||||
function fetch_ticket() {
|
function fetch_ticket() {
|
||||||
// Fetch the Kerberos Updated ticket through SPNEGO
|
// Fetch the Kerberos Updated ticket through SPNEGO
|
||||||
return fetch(url_for('authenticate.kerberos_update_ticket')
|
return fetch(url_for('kerberos.update_ticket')
|
||||||
)
|
)
|
||||||
.then(function(response){
|
.then(function(response){
|
||||||
if (response.status >= 200 && response.status < 300) {
|
if (response.status >= 200 && response.status < 300) {
|
||||||
@ -18,7 +27,7 @@ function fetch_ticket() {
|
|||||||
function fetch_ticket_lifetime () {
|
function fetch_ticket_lifetime () {
|
||||||
// Fetch the Kerberos ticket lifetime left
|
// Fetch the Kerberos ticket lifetime left
|
||||||
|
|
||||||
return fetch(url_for('authenticate.kerberos_validate_ticket')
|
return fetch(url_for('kerberos.validate_ticket')
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
function(response){
|
function(response){
|
||||||
|
@ -50,7 +50,7 @@ from pgadmin.utils.master_password import validate_master_password, \
|
|||||||
set_crypt_key, process_masterpass_disabled
|
set_crypt_key, process_masterpass_disabled
|
||||||
from pgadmin.model import User
|
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, LDAP, QT_DEFAULT_PLACEHOLDER
|
INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER, OAUTH2
|
||||||
from pgadmin.authenticate import AuthSourceManager
|
from pgadmin.authenticate import AuthSourceManager
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -607,14 +607,8 @@ class BrowserPluginModule(PgAdminModule):
|
|||||||
|
|
||||||
|
|
||||||
def _get_logout_url():
|
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(
|
return '{0}?next={1}'.format(
|
||||||
url_for('security.logout'), url_for(BROWSER_INDEX))
|
url_for(current_app.login_manager.logout_view), url_for(BROWSER_INDEX))
|
||||||
|
|
||||||
|
|
||||||
def _get_supported_browser():
|
def _get_supported_browser():
|
||||||
@ -748,10 +742,10 @@ def index():
|
|||||||
if len(config.AUTHENTICATION_SOURCES) == 1\
|
if len(config.AUTHENTICATION_SOURCES) == 1\
|
||||||
and INTERNAL in config.AUTHENTICATION_SOURCES:
|
and INTERNAL in config.AUTHENTICATION_SOURCES:
|
||||||
auth_only_internal = True
|
auth_only_internal = True
|
||||||
auth_source = session['_auth_source_manager_obj'][
|
auth_source = session['auth_source_manager'][
|
||||||
'source_friendly_name']
|
'source_friendly_name']
|
||||||
|
|
||||||
if session['_auth_source_manager_obj']['current_source'] == KERBEROS:
|
if not config.MASTER_PASSWORD_REQUIRED and 'pass_enc_key' in session:
|
||||||
session['allow_save_password'] = False
|
session['allow_save_password'] = False
|
||||||
|
|
||||||
response = Response(render_template(
|
response = Response(render_template(
|
||||||
@ -877,7 +871,8 @@ def app_constants():
|
|||||||
render_template('browser/js/constants.js',
|
render_template('browser/js/constants.js',
|
||||||
INTERNAL=INTERNAL,
|
INTERNAL=INTERNAL,
|
||||||
LDAP=LDAP,
|
LDAP=LDAP,
|
||||||
KERBEROS=KERBEROS),
|
KERBEROS=KERBEROS,
|
||||||
|
OAUTH2=OAUTH2),
|
||||||
200, {'Content-Type': MIMETYPE_APP_JS}
|
200, {'Content-Type': MIMETYPE_APP_JS}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1005,8 +1000,9 @@ def set_master_password():
|
|||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
|
|
||||||
# Master password is not applicable for server mode
|
# Master password is not applicable for server mode
|
||||||
if not config.SERVER_MODE and config.MASTER_PASSWORD_REQUIRED:
|
# Enable master password if oauth is used
|
||||||
|
if not config.SERVER_MODE or OAUTH2 in config.AUTHENTICATION_SOURCES\
|
||||||
|
and config.MASTER_PASSWORD_REQUIRED:
|
||||||
# if master pass is set previously
|
# if master pass is set previously
|
||||||
if current_user.masterpass_check is not None and \
|
if current_user.masterpass_check is not None and \
|
||||||
data.get('button_click') and \
|
data.get('button_click') and \
|
||||||
@ -1043,7 +1039,7 @@ def set_master_password():
|
|||||||
existing=True,
|
existing=True,
|
||||||
present=False,
|
present=False,
|
||||||
)
|
)
|
||||||
elif not get_crypt_key()[0]:
|
elif not get_crypt_key()[1]:
|
||||||
error_message = None
|
error_message = None
|
||||||
if data.get('button_click') and data.get('password') == '':
|
if data.get('button_click') and data.get('password') == '':
|
||||||
# If user attempted to enter a blank password, then throw error
|
# If user attempted to enter a blank password, then throw error
|
||||||
@ -1334,6 +1330,9 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
|||||||
|
|
||||||
do_flash(*get_message('PASSWORD_RESET'))
|
do_flash(*get_message('PASSWORD_RESET'))
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
auth_obj = AuthSourceManager(form, [INTERNAL])
|
||||||
|
session['auth_source_manager'] = auth_obj.as_dict()
|
||||||
|
|
||||||
return redirect(get_url(_security.post_reset_view) or
|
return redirect(get_url(_security.post_reset_view) or
|
||||||
get_url(_security.post_login_view))
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#
|
#
|
||||||
##########################################################################
|
##########################################################################
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from regression.python_test_utils import test_utils
|
from regression.python_test_utils import test_utils
|
||||||
@ -24,20 +25,21 @@ class TestRoleDependenciesSql(SQLTemplateTestBase):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(TestRoleDependenciesSql, self).__init__()
|
super(TestRoleDependenciesSql, self).__init__()
|
||||||
self.table_id = -1
|
self.table_id = -1
|
||||||
|
self.role_name = "testpgadmin%s" % str(uuid.uuid4())[1:8]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
with test_utils.Database(self.server) as (connection, database_name):
|
with test_utils.Database(self.server) as (connection, database_name):
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"CREATE ROLE testpgadmin LOGIN PASSWORD '%s'"
|
"CREATE ROLE %s LOGIN PASSWORD '%s'"
|
||||||
% self.server['db_password'])
|
% (self.role_name, self.server['db_password']))
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
print(exception)
|
print(exception)
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
self.server_with_modified_user = self.server.copy()
|
self.server_with_modified_user = self.server.copy()
|
||||||
self.server_with_modified_user['username'] = "testpgadmin"
|
self.server_with_modified_user['username'] = self.role_name
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
if hasattr(self, "ignore_test"):
|
if hasattr(self, "ignore_test"):
|
||||||
@ -61,7 +63,7 @@ class TestRoleDependenciesSql(SQLTemplateTestBase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
with test_utils.Database(self.server) as (connection, database_name):
|
with test_utils.Database(self.server) as (connection, database_name):
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute("DROP ROLE testpgadmin")
|
cursor.execute("DROP ROLE %s" % self.role_name)
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
def generate_sql(self, version):
|
def generate_sql(self, version):
|
||||||
|
@ -12,6 +12,7 @@ define('pgadmin.browser.constants', [], function() {
|
|||||||
return {
|
return {
|
||||||
'INTERNAL': '{{ INTERNAL }}',
|
'INTERNAL': '{{ INTERNAL }}',
|
||||||
'LDAP': '{{ LDAP }}',
|
'LDAP': '{{ LDAP }}',
|
||||||
'KERBEROS': '{{ KERBEROS }}'
|
'KERBEROS': '{{ KERBEROS }}',
|
||||||
|
'OAUTH2': '{{ OAUTH2 }}'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ from regression.python_test_utils import test_utils as utils
|
|||||||
from pgadmin.authenticate.registry import AuthSourceRegistry
|
from pgadmin.authenticate.registry import AuthSourceRegistry
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
|
from pgadmin.utils.constants import LDAP, INTERNAL, KERBEROS
|
||||||
|
|
||||||
|
|
||||||
class KerberosLoginMockTestCase(BaseTestGenerator):
|
class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||||
@ -23,17 +24,17 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
|||||||
|
|
||||||
scenarios = [
|
scenarios = [
|
||||||
('Spnego/Kerberos Authentication: Test Unauthorized', dict(
|
('Spnego/Kerberos Authentication: Test Unauthorized', dict(
|
||||||
auth_source=['kerberos'],
|
auth_source=[KERBEROS],
|
||||||
auto_create_user=True,
|
auto_create_user=True,
|
||||||
flag=1
|
flag=1
|
||||||
)),
|
)),
|
||||||
('Spnego/Kerberos Authentication: Test Authorized', dict(
|
('Spnego/Kerberos Authentication: Test Authorized', dict(
|
||||||
auth_source=['kerberos'],
|
auth_source=[KERBEROS],
|
||||||
auto_create_user=True,
|
auto_create_user=True,
|
||||||
flag=2
|
flag=2
|
||||||
)),
|
)),
|
||||||
('Spnego/Kerberos Update Ticket', dict(
|
('Spnego/Kerberos Update Ticket', dict(
|
||||||
auth_source=['kerberos'],
|
auth_source=[KERBEROS],
|
||||||
auto_create_user=True,
|
auto_create_user=True,
|
||||||
flag=3
|
flag=3
|
||||||
))
|
))
|
||||||
@ -49,7 +50,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
app_config.AUTHENTICATION_SOURCES = self.auth_source
|
app_config.AUTHENTICATION_SOURCES = self.auth_source
|
||||||
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'kerberos'
|
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = KERBEROS
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
"""This function checks spnego/kerberos login functionality."""
|
"""This function checks spnego/kerberos login functionality."""
|
||||||
@ -100,14 +101,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
|||||||
self.initiator_name = 'user@PGADMIN.ORG'
|
self.initiator_name = 'user@PGADMIN.ORG'
|
||||||
|
|
||||||
del_crads = delCrads()
|
del_crads = delCrads()
|
||||||
|
AuthSourceRegistry._registry[KERBEROS].negotiate_start = MagicMock(
|
||||||
AuthSourceRegistry._registry['kerberos'].negotiate_start = MagicMock(
|
|
||||||
return_value=[True, del_crads])
|
return_value=[True, del_crads])
|
||||||
return del_crads
|
return del_crads
|
||||||
|
|
||||||
def test_update_ticket(self):
|
def test_update_ticket(self):
|
||||||
# Response header should include the Negotiate header in the first call
|
# Response header should include the Negotiate header in the first call
|
||||||
response = self.tester.get('/authenticate/kerberos/update_ticket')
|
response = self.tester.get('/kerberos/update_ticket')
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate')
|
self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate')
|
||||||
|
|
||||||
@ -117,12 +117,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
|||||||
krb_token = Headers({})
|
krb_token = Headers({})
|
||||||
krb_token['Authorization'] = 'Negotiate CTOKEN'
|
krb_token['Authorization'] = 'Negotiate CTOKEN'
|
||||||
|
|
||||||
response = self.tester.get('/authenticate/kerberos/update_ticket',
|
response = self.tester.get('/kerberos/update_ticket',
|
||||||
headers=krb_token)
|
headers=krb_token)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.tester.logout()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
@ -130,5 +131,6 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
|||||||
We need to again login the test client as soon as test scenarios
|
We need to again login the test client as soon as test scenarios
|
||||||
finishes.
|
finishes.
|
||||||
"""
|
"""
|
||||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
cls.tester.logout()
|
||||||
|
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||||
utils.login_tester_account(cls.tester)
|
utils.login_tester_account(cls.tester)
|
||||||
|
@ -11,6 +11,7 @@ import config as app_config
|
|||||||
from pgadmin.utils.route import BaseTestGenerator
|
from pgadmin.utils.route import BaseTestGenerator
|
||||||
from regression.python_test_utils import test_utils as utils
|
from regression.python_test_utils import test_utils as utils
|
||||||
from regression.test_setup import config_data
|
from regression.test_setup import config_data
|
||||||
|
from pgadmin.utils.constants import LDAP, INTERNAL
|
||||||
|
|
||||||
|
|
||||||
class LDAPLoginTestCase(BaseTestGenerator):
|
class LDAPLoginTestCase(BaseTestGenerator):
|
||||||
@ -50,7 +51,7 @@ class LDAPLoginTestCase(BaseTestGenerator):
|
|||||||
ldap_config = config_data['ldap_config'][0][self.config_key_param]
|
ldap_config = config_data['ldap_config'][0][self.config_key_param]
|
||||||
except (KeyError, TypeError, IndexError):
|
except (KeyError, TypeError, IndexError):
|
||||||
self.skipTest("LDAP config not set.")
|
self.skipTest("LDAP config not set.")
|
||||||
app_config.AUTHENTICATION_SOURCES = ['ldap']
|
app_config.AUTHENTICATION_SOURCES = [LDAP]
|
||||||
app_config.LDAP_AUTO_CREATE_USER = True
|
app_config.LDAP_AUTO_CREATE_USER = True
|
||||||
app_config.LDAP_SERVER_URI = ldap_config['uri']
|
app_config.LDAP_SERVER_URI = ldap_config['uri']
|
||||||
app_config.LDAP_BASE_DN = ldap_config['base_dn']
|
app_config.LDAP_BASE_DN = ldap_config['base_dn']
|
||||||
@ -70,6 +71,7 @@ class LDAPLoginTestCase(BaseTestGenerator):
|
|||||||
if ldap_config['anonymous_bind'] != "" and\
|
if ldap_config['anonymous_bind'] != "" and\
|
||||||
ldap_config['anonymous_bind']:
|
ldap_config['anonymous_bind']:
|
||||||
app_config.LDAP_ANONYMOUS_BIND = True
|
app_config.LDAP_ANONYMOUS_BIND = True
|
||||||
|
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
"""This function checks login functionality."""
|
"""This function checks login functionality."""
|
||||||
@ -92,5 +94,5 @@ class LDAPLoginTestCase(BaseTestGenerator):
|
|||||||
finishes.
|
finishes.
|
||||||
"""
|
"""
|
||||||
cls.tester.logout()
|
cls.tester.logout()
|
||||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||||
utils.login_tester_account(cls.tester)
|
utils.login_tester_account(cls.tester)
|
||||||
|
@ -13,6 +13,7 @@ from regression.python_test_utils import test_utils as utils
|
|||||||
from regression.test_setup import config_data
|
from regression.test_setup import config_data
|
||||||
from pgadmin.authenticate.registry import AuthSourceRegistry
|
from pgadmin.authenticate.registry import AuthSourceRegistry
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from pgadmin.utils.constants import LDAP, INTERNAL
|
||||||
|
|
||||||
|
|
||||||
class LDAPLoginMockTestCase(BaseTestGenerator):
|
class LDAPLoginMockTestCase(BaseTestGenerator):
|
||||||
@ -23,17 +24,17 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
|
|||||||
|
|
||||||
scenarios = [
|
scenarios = [
|
||||||
('LDAP Authentication with Auto Create User', dict(
|
('LDAP Authentication with Auto Create User', dict(
|
||||||
auth_source=['ldap'],
|
auth_source=[LDAP],
|
||||||
auto_create_user=True,
|
auto_create_user=True,
|
||||||
username='ldap_user',
|
username='ldap_user',
|
||||||
password='ldap_pass')),
|
password='ldap_pass')),
|
||||||
('LDAP Authentication without Auto Create User', dict(
|
('LDAP Authentication without Auto Create User', dict(
|
||||||
auth_source=['ldap'],
|
auth_source=[LDAP],
|
||||||
auto_create_user=False,
|
auto_create_user=False,
|
||||||
username='ldap_user',
|
username='ldap_user',
|
||||||
password='ldap_pass')),
|
password='ldap_pass')),
|
||||||
('LDAP + Internal Authentication', dict(
|
('LDAP + Internal Authentication', dict(
|
||||||
auth_source=['ldap', 'internal'],
|
auth_source=[LDAP, INTERNAL],
|
||||||
auto_create_user=False,
|
auto_create_user=False,
|
||||||
username=config_data[
|
username=config_data[
|
||||||
'pgAdmin4_login_credentials']['login_username'],
|
'pgAdmin4_login_credentials']['login_username'],
|
||||||
@ -56,14 +57,15 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
|
|||||||
app_config.LDAP_ANONYMOUS_BIND = False
|
app_config.LDAP_ANONYMOUS_BIND = False
|
||||||
app_config.LDAP_BIND_USER = None
|
app_config.LDAP_BIND_USER = None
|
||||||
app_config.LDAP_BIND_PASSWORD = None
|
app_config.LDAP_BIND_PASSWORD = None
|
||||||
|
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP
|
||||||
|
|
||||||
@patch.object(AuthSourceRegistry._registry['ldap'], 'connect',
|
@patch.object(AuthSourceRegistry._registry[LDAP], 'connect',
|
||||||
return_value=[True, "Done"])
|
return_value=[True, "Done"])
|
||||||
@patch.object(AuthSourceRegistry._registry['ldap'], 'search_ldap_user',
|
@patch.object(AuthSourceRegistry._registry[LDAP], 'search_ldap_user',
|
||||||
return_value=[True, ''])
|
return_value=[True, ''])
|
||||||
def runTest(self, conn_mock_obj, search_mock_obj):
|
def runTest(self, conn_mock_obj, search_mock_obj):
|
||||||
"""This function checks ldap login functionality."""
|
"""This function checks ldap login functionality."""
|
||||||
AuthSourceRegistry._registry['ldap'].dedicated_user = False
|
AuthSourceRegistry._registry[LDAP].dedicated_user = False
|
||||||
res = self.tester.login(self.username, self.password, True)
|
res = self.tester.login(self.username, self.password, True)
|
||||||
respdata = 'Gravatar image for %s' % self.username
|
respdata = 'Gravatar image for %s' % self.username
|
||||||
self.assertTrue(respdata in res.data.decode('utf8'))
|
self.assertTrue(respdata in res.data.decode('utf8'))
|
||||||
@ -78,5 +80,5 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
|
|||||||
finishes.
|
finishes.
|
||||||
"""
|
"""
|
||||||
cls.tester.logout()
|
cls.tester.logout()
|
||||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||||
utils.login_tester_account(cls.tester)
|
utils.login_tester_account(cls.tester)
|
||||||
|
@ -12,6 +12,7 @@ import config as app_config
|
|||||||
from pgadmin.utils.route import BaseTestGenerator
|
from pgadmin.utils.route import BaseTestGenerator
|
||||||
from regression.python_test_utils import test_utils as utils
|
from regression.python_test_utils import test_utils as utils
|
||||||
from regression.test_setup import config_data
|
from regression.test_setup import config_data
|
||||||
|
from pgadmin.utils.constants import INTERNAL
|
||||||
|
|
||||||
|
|
||||||
class LoginTestCase(BaseTestGenerator):
|
class LoginTestCase(BaseTestGenerator):
|
||||||
@ -98,7 +99,7 @@ class LoginTestCase(BaseTestGenerator):
|
|||||||
|
|
||||||
# No need to call base class setup function
|
# No need to call base class setup function
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
pass
|
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
"""This function checks login functionality."""
|
"""This function checks login functionality."""
|
||||||
|
@ -11,6 +11,7 @@ import json
|
|||||||
|
|
||||||
from pgadmin.utils.route import BaseTestGenerator
|
from pgadmin.utils.route import BaseTestGenerator
|
||||||
import config
|
import config
|
||||||
|
from pgadmin.utils.constants import INTERNAL
|
||||||
|
|
||||||
|
|
||||||
class MasterPasswordTestCase(BaseTestGenerator):
|
class MasterPasswordTestCase(BaseTestGenerator):
|
||||||
@ -53,6 +54,7 @@ class MasterPasswordTestCase(BaseTestGenerator):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
config.MASTER_PASSWORD_REQUIRED = True
|
config.MASTER_PASSWORD_REQUIRED = True
|
||||||
|
config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
"""This function will check change password functionality."""
|
"""This function will check change password functionality."""
|
||||||
|
147
web/pgadmin/browser/tests/test_oauth2_with_mocking.py
Normal file
147
web/pgadmin/browser/tests/test_oauth2_with_mocking.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
##########################################################################
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class Oauth2LoginMockTestCase(BaseTestGenerator):
|
||||||
|
"""
|
||||||
|
This class checks oauth2 login functionality by mocking
|
||||||
|
External Oauth2 Authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
scenarios = [
|
||||||
|
('Oauth2 External Authentication', dict(
|
||||||
|
auth_source=['oauth2'],
|
||||||
|
oauth2_provider='github',
|
||||||
|
flag=1
|
||||||
|
)),
|
||||||
|
('Oauth2 Authentication', dict(
|
||||||
|
auth_source=['oauth2'],
|
||||||
|
oauth2_provider='github',
|
||||||
|
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 = OAUTH2
|
||||||
|
app_config.OAUTH2_CONFIG = [
|
||||||
|
{
|
||||||
|
'OAUTH2_NAME': 'github',
|
||||||
|
'OAUTH2_DISPLAY_NAME': 'Github',
|
||||||
|
'OAUTH2_CLIENT_ID': 'testclientid',
|
||||||
|
'OAUTH2_CLIENT_SECRET': 'testclientsec',
|
||||||
|
'OAUTH2_TOKEN_URL':
|
||||||
|
'https://github.com/login/oauth/access_token',
|
||||||
|
'OAUTH2_AUTHORIZATION_URL':
|
||||||
|
'https://github.com/login/oauth/authorize',
|
||||||
|
'OAUTH2_API_BASE_URL': 'https://api.github.com/',
|
||||||
|
'OAUTH2_USERINFO_ENDPOINT': 'user',
|
||||||
|
'OAUTH2_ICON': 'fa-github',
|
||||||
|
'OAUTH2_BUTTON_COLOR': '#3253a8',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def runTest(self):
|
||||||
|
"""This function checks oauth2 login functionality."""
|
||||||
|
if app_config.SERVER_MODE is False:
|
||||||
|
self.skipTest(
|
||||||
|
"Can not run Oauth2 Authentication in the Desktop mode."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.flag == 1:
|
||||||
|
self.test_external_authentication()
|
||||||
|
elif self.flag == 2:
|
||||||
|
self.test_oauth2_authentication()
|
||||||
|
|
||||||
|
def test_external_authentication(self):
|
||||||
|
"""
|
||||||
|
Ensure that the user should be redirected
|
||||||
|
to the external url for the authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AuthSourceManager.update_auth_sources = MagicMock()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.tester.login(
|
||||||
|
email=None, password=None,
|
||||||
|
_follow_redirects=True,
|
||||||
|
headers=None,
|
||||||
|
extra_form_data=dict(oauth2_button=self.oauth2_provider)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.assertEqual('Following external'
|
||||||
|
' redirects is not supported.', str(e))
|
||||||
|
|
||||||
|
def test_oauth2_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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
profile = self.mock_user_profile()
|
||||||
|
|
||||||
|
# Mock Oauth2 Authenticate
|
||||||
|
AuthSourceRegistry._registry[OAUTH2].authenticate = MagicMock(
|
||||||
|
return_value=[True, ''])
|
||||||
|
|
||||||
|
AuthSourceManager.update_auth_sources = MagicMock()
|
||||||
|
|
||||||
|
# Create AuthSourceManager object
|
||||||
|
auth_obj = AuthSourceManager({}, [OAUTH2])
|
||||||
|
auth_source = AuthSourceRegistry.get(OAUTH2)
|
||||||
|
auth_obj.set_source(auth_source)
|
||||||
|
auth_obj.set_current_source(auth_source.get_source_name())
|
||||||
|
|
||||||
|
# Check the login with Oauth2
|
||||||
|
res = self.tester.login(email=None, password=None,
|
||||||
|
_follow_redirects=True,
|
||||||
|
headers=None,
|
||||||
|
extra_form_data=dict(
|
||||||
|
oauth2_button=self.oauth2_provider)
|
||||||
|
)
|
||||||
|
|
||||||
|
respdata = 'Gravatar image for %s' % profile['email']
|
||||||
|
self.assertTrue(respdata in res.data.decode('utf8'))
|
||||||
|
|
||||||
|
def mock_user_profile(self):
|
||||||
|
profile = {'email': 'oauth2@gmail.com'}
|
||||||
|
|
||||||
|
AuthSourceRegistry._registry[OAUTH2].get_user_profile = MagicMock(
|
||||||
|
return_value=profile)
|
||||||
|
return profile
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
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)
|
@ -280,7 +280,7 @@ class BatchProcess(object):
|
|||||||
env['OUTDIR'] = self.log_dir
|
env['OUTDIR'] = self.log_dir
|
||||||
env['PGA_BGP_FOREGROUND'] = "1"
|
env['PGA_BGP_FOREGROUND'] = "1"
|
||||||
if config.SERVER_MODE and session and \
|
if config.SERVER_MODE and session and \
|
||||||
session['_auth_source_manager_obj']['current_source'] == \
|
session['auth_source_manager']['current_source'] == \
|
||||||
KERBEROS:
|
KERBEROS:
|
||||||
env['KRB5CCNAME'] = session['KRB5CCNAME']
|
env['KRB5CCNAME'] = session['KRB5CCNAME']
|
||||||
|
|
||||||
|
@ -946,6 +946,9 @@ table.table-empty-rows{
|
|||||||
& .btn-login {
|
& .btn-login {
|
||||||
background-color: $security-btn-color;
|
background-color: $security-btn-color;
|
||||||
}
|
}
|
||||||
|
& .btn-oauth {
|
||||||
|
background-color: $security-btn-color;
|
||||||
|
}
|
||||||
& .user-language {
|
& .user-language {
|
||||||
& select{
|
& select{
|
||||||
background-color: $color-primary;
|
background-color: $color-primary;
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
|
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
|
||||||
{{ render_username_with_errors(login_user_form.email, "text") }}
|
{{ render_username_with_errors(login_user_form.email, "text") }}
|
||||||
{{ render_field_with_errors(login_user_form.password, "password") }}
|
{{ render_field_with_errors(login_user_form.password, "password") }}
|
||||||
<button class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Login') }}">{{ _('Login') }}</button>
|
<button name="internal_button" {% if (config.OAUTH2 in config.AUTHENTICATION_SOURCES or config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 1 %} disabled {% endif %} class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Login') }}">{{ _('Login') }}</button>
|
||||||
<div class="form-group row mb-3 c user-language">
|
<div class="form-group row mb-3 c user-language">
|
||||||
<div class="col-7"><span class="help-block">{{ _('<a href="%(url)s" class="text-white">Forgotten your password</a>?', url=url_for('browser.forgot_password')) }}</span></div>
|
<div class="col-7"><span class="help-block">{{ _('<a href="%(url)s" class="text-white">Forgotten your password</a>?', url=url_for('browser.forgot_password')) }}</span></div>
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
@ -20,9 +20,16 @@
|
|||||||
{% for key, lang in config.LANGUAGES.items() %}
|
{% for key, lang in config.LANGUAGES.items() %}
|
||||||
<option value="{{key}}" {% if user_language == key %}selected{% endif %}>{{lang}}</option>
|
<option value="{{key}}" {% if user_language == key %}selected{% endif %}>{{lang}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.AUTHENTICATION_SOURCES %}
|
||||||
|
{% for oauth_config in config.OAUTH2_CONFIG %}
|
||||||
|
<button name="oauth2_button" class="btn btn-primary btn-block btn-oauth" style="background-color: {{oauth_config['OAUTH2_BUTTON_COLOR']}}" value="{{ oauth_config['OAUTH2_NAME'] }}" type="submit">
|
||||||
|
<i class="fab {{ oauth_config['OAUTH2_ICON'] }} fa-lg mr-2" aria-hidden="true" role="image"></i>
|
||||||
|
{{ _('Login with %(oauth_name)s', oauth_name=oauth_config['OAUTH2_DISPLAY_NAME']) }}</button>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -176,7 +176,7 @@ def current_user_info():
|
|||||||
config.ALLOW_SAVE_TUNNEL_PASSWORD and session[
|
config.ALLOW_SAVE_TUNNEL_PASSWORD and session[
|
||||||
'allow_save_password'] else 'false',
|
'allow_save_password'] else 'false',
|
||||||
auth_sources=config.AUTHENTICATION_SOURCES,
|
auth_sources=config.AUTHENTICATION_SOURCES,
|
||||||
current_auth_source=session['_auth_source_manager_obj'][
|
current_auth_source=session['auth_source_manager'][
|
||||||
'current_source'] if config.SERVER_MODE is True else INTERNAL
|
'current_source'] if config.SERVER_MODE is True else INTERNAL
|
||||||
),
|
),
|
||||||
status=200,
|
status=200,
|
||||||
|
@ -28,6 +28,7 @@ define([
|
|||||||
DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'],
|
DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'],
|
||||||
LDAP = pgConst['LDAP'],
|
LDAP = pgConst['LDAP'],
|
||||||
KERBEROS = pgConst['KERBEROS'],
|
KERBEROS = pgConst['KERBEROS'],
|
||||||
|
OAUTH2 = pgConst['OAUTH2'],
|
||||||
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false,
|
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false,
|
||||||
userFilter = function(collection) {
|
userFilter = function(collection) {
|
||||||
return (new Backgrid.Extension.ClientSideFilter({
|
return (new Backgrid.Extension.ClientSideFilter({
|
||||||
@ -607,6 +608,16 @@ define([
|
|||||||
this.get('username')
|
this.get('username')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.errorModel.set('username', errmsg);
|
||||||
|
return errmsg;
|
||||||
|
}
|
||||||
|
else if (!!this.get('username') && this.collection.nonFilter.where({
|
||||||
|
'username': this.get('username'), 'auth_source': OAUTH2,
|
||||||
|
}).length > 1) {
|
||||||
|
errmsg = gettext('The username %s already exists.',
|
||||||
|
this.get('username')
|
||||||
|
);
|
||||||
|
|
||||||
this.errorModel.set('username', errmsg);
|
this.errorModel.set('username', errmsg);
|
||||||
return errmsg;
|
return errmsg;
|
||||||
}
|
}
|
||||||
@ -1053,7 +1064,7 @@ define([
|
|||||||
saveUser: function(m) {
|
saveUser: function(m) {
|
||||||
var d = m.toJSON(true);
|
var d = m.toJSON(true);
|
||||||
|
|
||||||
if((m.isNew() && (m.get('auth_source') == LDAP || m.get('auth_source') == KERBEROS) && (!m.get('username') || !m.get('auth_source') || !m.get('role')))
|
if((m.isNew() && (m.get('auth_source') == LDAP || m.get('auth_source') == KERBEROS || m.get('auth_source') == OAUTH2) && (!m.get('username') || !m.get('auth_source') || !m.get('role')))
|
||||||
|| (m.isNew() && m.get('auth_source') == DEFAULT_AUTH_SOURCE && (!m.get('email') || !m.get('role') ||
|
|| (m.isNew() && m.get('auth_source') == DEFAULT_AUTH_SOURCE && (!m.get('email') || !m.get('role') ||
|
||||||
!m.get('newPassword') || !m.get('confirmPassword') || m.get('newPassword') != m.get('confirmPassword')))
|
!m.get('newPassword') || !m.get('confirmPassword') || m.get('newPassword') != m.get('confirmPassword')))
|
||||||
|| (!m.isNew() && m.get('newPassword') != m.get('confirmPassword'))) {
|
|| (!m.isNew() && m.get('newPassword') != m.get('confirmPassword'))) {
|
||||||
|
@ -55,10 +55,12 @@ ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
|
|||||||
INTERNAL = 'internal'
|
INTERNAL = 'internal'
|
||||||
LDAP = 'ldap'
|
LDAP = 'ldap'
|
||||||
KERBEROS = 'kerberos'
|
KERBEROS = 'kerberos'
|
||||||
|
OAUTH2 = "oauth2"
|
||||||
|
|
||||||
SUPPORTED_AUTH_SOURCES = [INTERNAL,
|
SUPPORTED_AUTH_SOURCES = [INTERNAL,
|
||||||
LDAP,
|
LDAP,
|
||||||
KERBEROS]
|
KERBEROS,
|
||||||
|
OAUTH2]
|
||||||
|
|
||||||
BINARY_PATHS = {
|
BINARY_PATHS = {
|
||||||
"as_bin_paths": [
|
"as_bin_paths": [
|
||||||
|
@ -319,7 +319,7 @@ class Connection(BaseConnection):
|
|||||||
config.APP_NAME, conn_id)
|
config.APP_NAME, conn_id)
|
||||||
|
|
||||||
if config.SERVER_MODE and \
|
if config.SERVER_MODE and \
|
||||||
session['_auth_source_manager_obj']['current_source'] == \
|
session['auth_source_manager']['current_source'] == \
|
||||||
KERBEROS and 'KRB5CCNAME' in session\
|
KERBEROS and 'KRB5CCNAME' in session\
|
||||||
and manager.kerberos_conn:
|
and manager.kerberos_conn:
|
||||||
lock.acquire()
|
lock.acquire()
|
||||||
@ -353,7 +353,7 @@ class Connection(BaseConnection):
|
|||||||
self._wait(pg_conn)
|
self._wait(pg_conn)
|
||||||
|
|
||||||
if config.SERVER_MODE and \
|
if config.SERVER_MODE and \
|
||||||
session['_auth_source_manager_obj']['current_source'] == \
|
session['auth_source_manager']['current_source'] == \
|
||||||
KERBEROS:
|
KERBEROS:
|
||||||
environ['KRB5CCNAME'] = ''
|
environ['KRB5CCNAME'] = ''
|
||||||
|
|
||||||
@ -378,7 +378,7 @@ class Connection(BaseConnection):
|
|||||||
return False, msg
|
return False, msg
|
||||||
finally:
|
finally:
|
||||||
if config.SERVER_MODE and \
|
if config.SERVER_MODE and \
|
||||||
session['_auth_source_manager_obj']['current_source'] == \
|
session['auth_source_manager']['current_source'] == \
|
||||||
KERBEROS and lock.locked():
|
KERBEROS and lock.locked():
|
||||||
lock.release()
|
lock.release()
|
||||||
|
|
||||||
|
@ -31,13 +31,11 @@ def get_crypt_key():
|
|||||||
return True, current_user.password
|
return True, current_user.password
|
||||||
# if desktop mode and master pass enabled
|
# if desktop mode and master pass enabled
|
||||||
elif config.MASTER_PASSWORD_REQUIRED \
|
elif config.MASTER_PASSWORD_REQUIRED \
|
||||||
and not config.SERVER_MODE and enc_key is None:
|
and enc_key is None:
|
||||||
return False, None
|
return False, None
|
||||||
elif config.SERVER_MODE and \
|
elif not config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
|
||||||
session['_auth_source_manager_obj']['current_source']\
|
'pass_enc_key' in session:
|
||||||
== KERBEROS:
|
return True, session['pass_enc_key']
|
||||||
return True, session['kerberos_key'] if 'kerberos_key' in session \
|
|
||||||
else None
|
|
||||||
else:
|
else:
|
||||||
return True, enc_key
|
return True, enc_key
|
||||||
|
|
||||||
|
@ -92,9 +92,7 @@ class TestClient(testing.FlaskClient):
|
|||||||
# and make a test request context that has those cookies in it.
|
# and make a test request context that has those cookies in it.
|
||||||
environ_overrides = {}
|
environ_overrides = {}
|
||||||
self.cookie_jar.inject_wsgi(environ_overrides)
|
self.cookie_jar.inject_wsgi(environ_overrides)
|
||||||
with self.app.test_request_context(
|
with self.app.test_request_context():
|
||||||
"/login", environ_overrides=environ_overrides,
|
|
||||||
):
|
|
||||||
# Now, we call Flask-WTF's method of generating a CSRF token...
|
# Now, we call Flask-WTF's method of generating a CSRF token...
|
||||||
csrf_token = generate_csrf()
|
csrf_token = generate_csrf()
|
||||||
# ...which also sets a value in `flask.session`, so we need to
|
# ...which also sets a value in `flask.session`, so we need to
|
||||||
@ -106,18 +104,27 @@ class TestClient(testing.FlaskClient):
|
|||||||
return csrf_token
|
return csrf_token
|
||||||
|
|
||||||
def login(self, email, password, _follow_redirects=False,
|
def login(self, email, password, _follow_redirects=False,
|
||||||
headers=None):
|
headers=None, extra_form_data=dict()):
|
||||||
|
csrf_token = None
|
||||||
if config.SERVER_MODE is True:
|
if config.SERVER_MODE is True:
|
||||||
res = self.get('/login', follow_redirects=True)
|
res = self.get('/login',
|
||||||
|
follow_redirects=_follow_redirects)
|
||||||
csrf_token = self.fetch_csrf(res)
|
csrf_token = self.fetch_csrf(res)
|
||||||
else:
|
|
||||||
|
if csrf_token is None:
|
||||||
csrf_token = self.generate_csrf_token()
|
csrf_token = self.generate_csrf_token()
|
||||||
|
|
||||||
|
form_data = dict(
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
csrf_token=csrf_token
|
||||||
|
)
|
||||||
|
|
||||||
|
if extra_form_data:
|
||||||
|
form_data.update(extra_form_data)
|
||||||
|
|
||||||
res = self.post(
|
res = self.post(
|
||||||
'/authenticate/login', data=dict(
|
'/authenticate/login', data=form_data,
|
||||||
email=email, password=password,
|
|
||||||
csrf_token=csrf_token,
|
|
||||||
),
|
|
||||||
follow_redirects=_follow_redirects,
|
follow_redirects=_follow_redirects,
|
||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
|
@ -1656,14 +1656,14 @@ def create_user(user_details):
|
|||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
user_details = (
|
user_details = (
|
||||||
user_details['login_username'], user_details['login_username'],
|
user_details['login_username'], user_details['login_username'],
|
||||||
user_details['login_password'], 1)
|
user_details['login_password'], 1, uuid.uuid4().hex)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
'select * from user where username = "%s"' % user_details[0])
|
'select * from user where username = "%s"' % user_details[0])
|
||||||
user = cur.fetchone()
|
user = cur.fetchone()
|
||||||
if user is None:
|
if user is None:
|
||||||
cur.execute('INSERT INTO user (username, email, password, active) '
|
cur.execute('INSERT INTO user (username, email, password, active,'
|
||||||
'VALUES (?,?,?,?)', user_details)
|
' fs_uniquifier) VALUES (?,?,?,?,?)', user_details)
|
||||||
user_id = cur.lastrowid
|
user_id = cur.lastrowid
|
||||||
conn.commit()
|
conn.commit()
|
||||||
else:
|
else:
|
||||||
|
@ -97,6 +97,7 @@ from regression.python_test_utils import test_utils
|
|||||||
from regression.python_test_utils.csrf_test_client import TestClient
|
from regression.python_test_utils.csrf_test_client import TestClient
|
||||||
|
|
||||||
config.SETTINGS_SCHEMA_VERSION = SCHEMA_VERSION
|
config.SETTINGS_SCHEMA_VERSION = SCHEMA_VERSION
|
||||||
|
from pgadmin.utils.constants import LDAP
|
||||||
|
|
||||||
# Override some other defaults
|
# Override some other defaults
|
||||||
from logging import WARNING
|
from logging import WARNING
|
||||||
@ -117,7 +118,7 @@ if config.SERVER_MODE is True:
|
|||||||
app.config['WTF_CSRF_ENABLED'] = True
|
app.config['WTF_CSRF_ENABLED'] = True
|
||||||
|
|
||||||
# Authentication sources
|
# Authentication sources
|
||||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
|
app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP
|
||||||
|
|
||||||
app.test_client_class = TestClient
|
app.test_client_class = TestClient
|
||||||
test_client = app.test_client()
|
test_client = app.test_client()
|
||||||
|
@ -9016,9 +9016,9 @@ watchpack@^2.0.0:
|
|||||||
glob-to-regexp "^0.4.1"
|
glob-to-regexp "^0.4.1"
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
|
|
||||||
"webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f":
|
"webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#06daee1a8111b4ed08d58670674a2967e4f68313":
|
||||||
version "2.2.5"
|
version "2.2.5"
|
||||||
resolved "git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f"
|
resolved "git+https://github.com/EnterpriseDB/wcDocker/#06daee1a8111b4ed08d58670674a2967e4f68313"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-free" "^5.14.0"
|
"@fortawesome/fontawesome-free" "^5.14.0"
|
||||||
FileSaver "^0.10.0"
|
FileSaver "^0.10.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user