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
|
||||
user-agents 2.2.0 MIT https://github.com/selwin/python-user-agents
|
||||
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
|
||||
shown for Python modules that are not required with this version.
|
||||
|
@ -37,6 +37,7 @@ Mode is pre-configured for security.
|
||||
change_user_password
|
||||
ldap
|
||||
kerberos
|
||||
oauth2
|
||||
|
||||
|
||||
.. 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
|
||||
*default_server* parameter."
|
||||
|
||||
|
||||
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
|
||||
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 #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 #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.
|
||||
|
||||
Housekeeping
|
||||
|
@ -41,3 +41,5 @@ eventlet==0.31.0
|
||||
httpagentparser==1.9.*
|
||||
user-agents==2.2.0
|
||||
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
|
||||
# External Supported Sources: ldap, kerberos
|
||||
# External Supported Sources: ldap, kerberos, oauth2
|
||||
# Multiple authentication can be achieved by setting this parameter to
|
||||
# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first,
|
||||
# in case of failure internal authentication will be done.
|
||||
# ['ldap', 'internal'] or ['oauth2', '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.
|
||||
|
||||
AUTHENTICATION_SOURCES = ['internal']
|
||||
|
||||
@ -666,6 +667,47 @@ KRB_AUTO_CREATE_USER = True
|
||||
|
||||
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
|
||||
##########################################################################
|
||||
|
@ -46,12 +46,13 @@ 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
|
||||
from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP
|
||||
|
||||
# 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
|
||||
# occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".
|
||||
import mimetypes
|
||||
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
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
|
||||
}))
|
||||
|
||||
app.config.update(dict({
|
||||
'INTERNAL': INTERNAL,
|
||||
'LDAP': LDAP,
|
||||
'KERBEROS': KERBEROS,
|
||||
'OAUTH2': OAUTH2
|
||||
}))
|
||||
|
||||
security.init_app(app, user_datastore)
|
||||
|
||||
# register custom unauthorised handler.
|
||||
@ -760,19 +768,18 @@ def create_app(app_name=None):
|
||||
)
|
||||
abort(401)
|
||||
login_user(user)
|
||||
elif config.SERVER_MODE and\
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\
|
||||
KERBEROS and \
|
||||
elif config.SERVER_MODE and \
|
||||
not current_user.is_authenticated and \
|
||||
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
|
||||
# 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 \
|
||||
KERBEROS and app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
|
||||
OAUTH2 and\
|
||||
current_app.keyManager.get() is None and \
|
||||
request.endpoint not in ('security.login', 'security.logout'):
|
||||
logout_user()
|
||||
|
@ -9,68 +9,33 @@
|
||||
|
||||
"""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
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils.constants import KERBEROS, INTERNAL
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
import copy
|
||||
|
||||
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'
|
||||
auth_obj = None
|
||||
|
||||
|
||||
class AuthenticateModule(PgAdminModule):
|
||||
def get_exposed_url_endpoints(self):
|
||||
return ['authenticate.login',
|
||||
'authenticate.kerberos_login',
|
||||
'authenticate.kerberos_logout',
|
||||
'authenticate.kerberos_update_ticket',
|
||||
'authenticate.kerberos_validate_ticket']
|
||||
return ['authenticate.login']
|
||||
|
||||
|
||||
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'])
|
||||
def login():
|
||||
"""
|
||||
@ -78,15 +43,20 @@ def login():
|
||||
The user input will be validated and authenticated.
|
||||
"""
|
||||
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
|
||||
if not auth_obj.validate():
|
||||
for field in form.errors:
|
||||
for error in form.errors[field]:
|
||||
flash(error, 'warning')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
return redirect(get_post_logout_redirect())
|
||||
|
||||
# Authenticate the user
|
||||
status, msg = auth_obj.authenticate()
|
||||
@ -94,34 +64,40 @@ def login():
|
||||
# Login the user
|
||||
status, msg = auth_obj.login()
|
||||
current_auth_obj = auth_obj.as_dict()
|
||||
|
||||
if not status:
|
||||
if current_auth_obj['current_source'] ==\
|
||||
KERBEROS:
|
||||
return flask.redirect('{0}?next={1}'.format(url_for(
|
||||
return redirect('{0}?next={1}'.format(url_for(
|
||||
'authenticate.kerberos_login'), url_for('browser.index')))
|
||||
|
||||
flash(msg, 'danger')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
|
||||
session['_auth_source_manager_obj'] = current_auth_obj
|
||||
return flask.redirect(get_post_login_redirect())
|
||||
return redirect(get_post_logout_redirect())
|
||||
session['auth_source_manager'] = current_auth_obj
|
||||
if 'auth_obj' in session:
|
||||
session['auth_obj'] = None
|
||||
return redirect(get_post_login_redirect())
|
||||
|
||||
elif isinstance(msg, Response):
|
||||
return msg
|
||||
elif 'oauth2_button' in request.form and not isinstance(msg, str):
|
||||
return msg
|
||||
flash(msg, 'danger')
|
||||
response = flask.redirect(get_post_logout_redirect())
|
||||
response = redirect(get_post_logout_redirect())
|
||||
return response
|
||||
|
||||
|
||||
class AuthSourceManager():
|
||||
class AuthSourceManager:
|
||||
"""This class will manage all the authentication sources.
|
||||
"""
|
||||
|
||||
def __init__(self, form, sources):
|
||||
self.form = form
|
||||
self.auth_sources = sources
|
||||
self.source = None
|
||||
self.source_friendly_name = INTERNAL
|
||||
self.current_source = None
|
||||
self.current_source = INTERNAL
|
||||
self.update_auth_sources()
|
||||
|
||||
def as_dict(self):
|
||||
"""
|
||||
@ -135,6 +111,14 @@ class AuthSourceManager():
|
||||
|
||||
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):
|
||||
self.current_source = source
|
||||
|
||||
@ -170,36 +154,19 @@ class AuthSourceManager():
|
||||
msg = None
|
||||
for src in self.auth_sources:
|
||||
source = get_auth_sources(src)
|
||||
self.set_source(source)
|
||||
current_app.logger.debug(
|
||||
"Authentication initiated via source: %s" %
|
||||
source.get_source_name())
|
||||
|
||||
if self.form.data['email'] and self.form.data['password'] and \
|
||||
source.get_source_name() == KERBEROS:
|
||||
msg = gettext('pgAdmin internal user authentication'
|
||||
' is not enabled, please contact administrator.')
|
||||
continue
|
||||
|
||||
status, msg = source.authenticate(self.form)
|
||||
|
||||
# When server sends Unauthorized header to get the ticket over HTTP
|
||||
# OR When kerberos authentication failed while accessing pgadmin,
|
||||
# we need to break the loop as no need to authenticate further
|
||||
# even if the authentication sources set to multiple
|
||||
if not status:
|
||||
if (hasattr(msg, 'status') and
|
||||
msg.status == '401 UNAUTHORIZED') or\
|
||||
(source.get_source_name() ==
|
||||
KERBEROS and
|
||||
request.method == 'GET'):
|
||||
break
|
||||
|
||||
if status:
|
||||
self.set_source(source)
|
||||
self.set_current_source(source.get_source_name())
|
||||
if msg is not None and 'username' in msg:
|
||||
self.form._fields['email'].data = msg['username']
|
||||
return status, msg
|
||||
|
||||
return status, msg
|
||||
|
||||
def login(self):
|
||||
@ -209,6 +176,13 @@ class AuthSourceManager():
|
||||
current_app.logger.debug(
|
||||
"Authentication and Login successfully done via source : %s" %
|
||||
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
|
||||
|
||||
|
||||
@ -239,58 +213,3 @@ def init_app(app):
|
||||
AuthSourceRegistry.load_modules(app)
|
||||
|
||||
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"""
|
||||
|
||||
import six
|
||||
from flask import current_app
|
||||
from flask import current_app, flash
|
||||
from flask_security import login_user
|
||||
from abc import abstractmethod, abstractproperty
|
||||
from flask_babelex import gettext
|
||||
@ -31,6 +31,8 @@ class BaseAuthentication(object):
|
||||
'PASSWORD_NOT_PROVIDED': gettext('Password not provided'),
|
||||
'INVALID_EMAIL': gettext('Email/Username is not valid')
|
||||
}
|
||||
LOGIN_VIEW = 'security.login'
|
||||
LOGOUT_VIEW = 'security.logout'
|
||||
|
||||
@abstractmethod
|
||||
def get_source_name(self):
|
||||
@ -97,7 +99,7 @@ class InternalAuthentication(BaseAuthentication):
|
||||
"""User validation"""
|
||||
# validate the email id first
|
||||
if not validate_email(form.data['email']):
|
||||
form.errors['email'] = [self.messages('INVALID_EMAIL')]
|
||||
flash(self.messages('INVALID_EMAIL'), 'warning')
|
||||
return False
|
||||
# Flask security validation
|
||||
return form.validate_on_submit()
|
||||
|
@ -10,22 +10,28 @@
|
||||
"""A blueprint module implementing the Spnego/Kerberos authentication."""
|
||||
|
||||
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 import Flask, request, Response, session,\
|
||||
current_app, render_template, flash
|
||||
from flask import request, Response, session,\
|
||||
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
|
||||
from pgadmin.model import User
|
||||
from pgadmin.tools.user_management import create_user
|
||||
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:
|
||||
import gssapi
|
||||
@ -46,8 +52,110 @@ if config.KRB_KTNAME and config.KRB_KTNAME != '<KRB5_KEYTAB_FILE>':
|
||||
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):
|
||||
|
||||
LOGIN_VIEW = 'kerberos.login'
|
||||
LOGOUT_VIEW = 'kerberos.logout'
|
||||
|
||||
def get_source_name(self):
|
||||
return KERBEROS
|
||||
|
||||
@ -85,7 +193,7 @@ class KerberosAuthentication(BaseAuthentication):
|
||||
if status:
|
||||
# Saving the first 15 characters of the kerberos key
|
||||
# to encrypt/decrypt database password
|
||||
session['kerberos_key'] = auth_header[1][0:15]
|
||||
session['pass_enc_key'] = auth_header[1][0:15]
|
||||
# Create user
|
||||
retval = self.__auto_create_user(
|
||||
str(negotiate.initiator_name))
|
||||
@ -162,7 +270,7 @@ class KerberosAuthentication(BaseAuthentication):
|
||||
username = str(username)
|
||||
if config.KRB_AUTO_CREATE_USER:
|
||||
user = User.query.filter_by(
|
||||
username=username).first()
|
||||
username=username, auth_source=KERBEROS).first()
|
||||
if user is None:
|
||||
return create_user({
|
||||
'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 userInfo from 'pgadmin.user_management.current_user';
|
||||
import pgConst from 'pgadmin.browser.constants';
|
||||
|
||||
function fetch_ticket() {
|
||||
// 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){
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
@ -18,7 +27,7 @@ function fetch_ticket() {
|
||||
function fetch_ticket_lifetime () {
|
||||
// Fetch the Kerberos ticket lifetime left
|
||||
|
||||
return fetch(url_for('authenticate.kerberos_validate_ticket')
|
||||
return fetch(url_for('kerberos.validate_ticket')
|
||||
)
|
||||
.then(
|
||||
function(response){
|
||||
|
@ -50,7 +50,7 @@ from pgadmin.utils.master_password import validate_master_password, \
|
||||
set_crypt_key, process_masterpass_disabled
|
||||
from pgadmin.model import User
|
||||
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
|
||||
|
||||
try:
|
||||
@ -607,14 +607,8 @@ class BrowserPluginModule(PgAdminModule):
|
||||
|
||||
|
||||
def _get_logout_url():
|
||||
if config.SERVER_MODE and\
|
||||
session['_auth_source_manager_obj']['current_source'] == \
|
||||
KERBEROS:
|
||||
return '{0}?next={1}'.format(url_for(
|
||||
'authenticate.kerberos_logout'), url_for(BROWSER_INDEX))
|
||||
|
||||
return '{0}?next={1}'.format(
|
||||
url_for('security.logout'), url_for(BROWSER_INDEX))
|
||||
url_for(current_app.login_manager.logout_view), url_for(BROWSER_INDEX))
|
||||
|
||||
|
||||
def _get_supported_browser():
|
||||
@ -748,10 +742,10 @@ def index():
|
||||
if len(config.AUTHENTICATION_SOURCES) == 1\
|
||||
and INTERNAL in config.AUTHENTICATION_SOURCES:
|
||||
auth_only_internal = True
|
||||
auth_source = session['_auth_source_manager_obj'][
|
||||
auth_source = session['auth_source_manager'][
|
||||
'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
|
||||
|
||||
response = Response(render_template(
|
||||
@ -877,7 +871,8 @@ def app_constants():
|
||||
render_template('browser/js/constants.js',
|
||||
INTERNAL=INTERNAL,
|
||||
LDAP=LDAP,
|
||||
KERBEROS=KERBEROS),
|
||||
KERBEROS=KERBEROS,
|
||||
OAUTH2=OAUTH2),
|
||||
200, {'Content-Type': MIMETYPE_APP_JS}
|
||||
)
|
||||
|
||||
@ -1005,8 +1000,9 @@ def set_master_password():
|
||||
data = json.loads(data)
|
||||
|
||||
# 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 current_user.masterpass_check is not None and \
|
||||
data.get('button_click') and \
|
||||
@ -1043,7 +1039,7 @@ def set_master_password():
|
||||
existing=True,
|
||||
present=False,
|
||||
)
|
||||
elif not get_crypt_key()[0]:
|
||||
elif not get_crypt_key()[1]:
|
||||
error_message = None
|
||||
if data.get('button_click') and data.get('password') == '':
|
||||
# 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'))
|
||||
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
|
||||
get_url(_security.post_login_view))
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
#
|
||||
##########################################################################
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import jinja2
|
||||
from regression.python_test_utils import test_utils
|
||||
@ -24,20 +25,21 @@ class TestRoleDependenciesSql(SQLTemplateTestBase):
|
||||
def __init__(self):
|
||||
super(TestRoleDependenciesSql, self).__init__()
|
||||
self.table_id = -1
|
||||
self.role_name = "testpgadmin%s" % str(uuid.uuid4())[1:8]
|
||||
|
||||
def setUp(self):
|
||||
with test_utils.Database(self.server) as (connection, database_name):
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"CREATE ROLE testpgadmin LOGIN PASSWORD '%s'"
|
||||
% self.server['db_password'])
|
||||
"CREATE ROLE %s LOGIN PASSWORD '%s'"
|
||||
% (self.role_name, self.server['db_password']))
|
||||
except Exception as exception:
|
||||
print(exception)
|
||||
connection.commit()
|
||||
|
||||
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):
|
||||
if hasattr(self, "ignore_test"):
|
||||
@ -61,7 +63,7 @@ class TestRoleDependenciesSql(SQLTemplateTestBase):
|
||||
def tearDown(self):
|
||||
with test_utils.Database(self.server) as (connection, database_name):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("DROP ROLE testpgadmin")
|
||||
cursor.execute("DROP ROLE %s" % self.role_name)
|
||||
connection.commit()
|
||||
|
||||
def generate_sql(self, version):
|
||||
|
@ -12,6 +12,7 @@ define('pgadmin.browser.constants', [], function() {
|
||||
return {
|
||||
'INTERNAL': '{{ INTERNAL }}',
|
||||
'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 unittest.mock import patch, MagicMock
|
||||
from werkzeug.datastructures import Headers
|
||||
from pgadmin.utils.constants import LDAP, INTERNAL, KERBEROS
|
||||
|
||||
|
||||
class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
@ -23,17 +24,17 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
|
||||
scenarios = [
|
||||
('Spnego/Kerberos Authentication: Test Unauthorized', dict(
|
||||
auth_source=['kerberos'],
|
||||
auth_source=[KERBEROS],
|
||||
auto_create_user=True,
|
||||
flag=1
|
||||
)),
|
||||
('Spnego/Kerberos Authentication: Test Authorized', dict(
|
||||
auth_source=['kerberos'],
|
||||
auth_source=[KERBEROS],
|
||||
auto_create_user=True,
|
||||
flag=2
|
||||
)),
|
||||
('Spnego/Kerberos Update Ticket', dict(
|
||||
auth_source=['kerberos'],
|
||||
auth_source=[KERBEROS],
|
||||
auto_create_user=True,
|
||||
flag=3
|
||||
))
|
||||
@ -49,7 +50,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
app_config.AUTHENTICATION_SOURCES = self.auth_source
|
||||
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'kerberos'
|
||||
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = KERBEROS
|
||||
|
||||
def runTest(self):
|
||||
"""This function checks spnego/kerberos login functionality."""
|
||||
@ -100,14 +101,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
self.initiator_name = 'user@PGADMIN.ORG'
|
||||
|
||||
del_crads = delCrads()
|
||||
|
||||
AuthSourceRegistry._registry['kerberos'].negotiate_start = MagicMock(
|
||||
AuthSourceRegistry._registry[KERBEROS].negotiate_start = MagicMock(
|
||||
return_value=[True, del_crads])
|
||||
return del_crads
|
||||
|
||||
def test_update_ticket(self):
|
||||
# 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.headers.get('www-authenticate'), 'Negotiate')
|
||||
|
||||
@ -117,12 +117,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
krb_token = Headers({})
|
||||
krb_token['Authorization'] = 'Negotiate CTOKEN'
|
||||
|
||||
response = self.tester.get('/authenticate/kerberos/update_ticket',
|
||||
response = self.tester.get('/kerberos/update_ticket',
|
||||
headers=krb_token)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.tester.logout()
|
||||
|
||||
def tearDown(self):
|
||||
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
@ -130,5 +131,6 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
We need to again login the test client as soon as test scenarios
|
||||
finishes.
|
||||
"""
|
||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
||||
cls.tester.logout()
|
||||
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||
utils.login_tester_account(cls.tester)
|
||||
|
@ -11,6 +11,7 @@ import config as app_config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.utils.constants import LDAP, INTERNAL
|
||||
|
||||
|
||||
class LDAPLoginTestCase(BaseTestGenerator):
|
||||
@ -50,7 +51,7 @@ class LDAPLoginTestCase(BaseTestGenerator):
|
||||
ldap_config = config_data['ldap_config'][0][self.config_key_param]
|
||||
except (KeyError, TypeError, IndexError):
|
||||
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_SERVER_URI = ldap_config['uri']
|
||||
app_config.LDAP_BASE_DN = ldap_config['base_dn']
|
||||
@ -70,6 +71,7 @@ class LDAPLoginTestCase(BaseTestGenerator):
|
||||
if ldap_config['anonymous_bind'] != "" and\
|
||||
ldap_config['anonymous_bind']:
|
||||
app_config.LDAP_ANONYMOUS_BIND = True
|
||||
self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP
|
||||
|
||||
def runTest(self):
|
||||
"""This function checks login functionality."""
|
||||
@ -92,5 +94,5 @@ class LDAPLoginTestCase(BaseTestGenerator):
|
||||
finishes.
|
||||
"""
|
||||
cls.tester.logout()
|
||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
||||
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||
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 pgadmin.authenticate.registry import AuthSourceRegistry
|
||||
from unittest.mock import patch
|
||||
from pgadmin.utils.constants import LDAP, INTERNAL
|
||||
|
||||
|
||||
class LDAPLoginMockTestCase(BaseTestGenerator):
|
||||
@ -23,17 +24,17 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
|
||||
|
||||
scenarios = [
|
||||
('LDAP Authentication with Auto Create User', dict(
|
||||
auth_source=['ldap'],
|
||||
auth_source=[LDAP],
|
||||
auto_create_user=True,
|
||||
username='ldap_user',
|
||||
password='ldap_pass')),
|
||||
('LDAP Authentication without Auto Create User', dict(
|
||||
auth_source=['ldap'],
|
||||
auth_source=[LDAP],
|
||||
auto_create_user=False,
|
||||
username='ldap_user',
|
||||
password='ldap_pass')),
|
||||
('LDAP + Internal Authentication', dict(
|
||||
auth_source=['ldap', 'internal'],
|
||||
auth_source=[LDAP, INTERNAL],
|
||||
auto_create_user=False,
|
||||
username=config_data[
|
||||
'pgAdmin4_login_credentials']['login_username'],
|
||||
@ -56,14 +57,15 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
|
||||
app_config.LDAP_ANONYMOUS_BIND = False
|
||||
app_config.LDAP_BIND_USER = 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"])
|
||||
@patch.object(AuthSourceRegistry._registry['ldap'], 'search_ldap_user',
|
||||
@patch.object(AuthSourceRegistry._registry[LDAP], 'search_ldap_user',
|
||||
return_value=[True, ''])
|
||||
def runTest(self, conn_mock_obj, search_mock_obj):
|
||||
"""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)
|
||||
respdata = 'Gravatar image for %s' % self.username
|
||||
self.assertTrue(respdata in res.data.decode('utf8'))
|
||||
@ -78,5 +80,5 @@ class LDAPLoginMockTestCase(BaseTestGenerator):
|
||||
finishes.
|
||||
"""
|
||||
cls.tester.logout()
|
||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
||||
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||
utils.login_tester_account(cls.tester)
|
||||
|
@ -12,6 +12,7 @@ import config as app_config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.utils.constants import INTERNAL
|
||||
|
||||
|
||||
class LoginTestCase(BaseTestGenerator):
|
||||
@ -98,7 +99,7 @@ class LoginTestCase(BaseTestGenerator):
|
||||
|
||||
# No need to call base class setup function
|
||||
def setUp(self):
|
||||
pass
|
||||
app_config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||
|
||||
def runTest(self):
|
||||
"""This function checks login functionality."""
|
||||
|
@ -11,6 +11,7 @@ import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
import config
|
||||
from pgadmin.utils.constants import INTERNAL
|
||||
|
||||
|
||||
class MasterPasswordTestCase(BaseTestGenerator):
|
||||
@ -53,6 +54,7 @@ class MasterPasswordTestCase(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
config.MASTER_PASSWORD_REQUIRED = True
|
||||
config.AUTHENTICATION_SOURCES = [INTERNAL]
|
||||
|
||||
def runTest(self):
|
||||
"""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['PGA_BGP_FOREGROUND'] = "1"
|
||||
if config.SERVER_MODE and session and \
|
||||
session['_auth_source_manager_obj']['current_source'] == \
|
||||
session['auth_source_manager']['current_source'] == \
|
||||
KERBEROS:
|
||||
env['KRB5CCNAME'] = session['KRB5CCNAME']
|
||||
|
||||
|
@ -946,6 +946,9 @@ table.table-empty-rows{
|
||||
& .btn-login {
|
||||
background-color: $security-btn-color;
|
||||
}
|
||||
& .btn-oauth {
|
||||
background-color: $security-btn-color;
|
||||
}
|
||||
& .user-language {
|
||||
& select{
|
||||
background-color: $color-primary;
|
||||
|
@ -12,7 +12,7 @@
|
||||
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
|
||||
{{ render_username_with_errors(login_user_form.email, "text") }}
|
||||
{{ 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="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">
|
||||
@ -20,9 +20,16 @@
|
||||
{% for key, lang in config.LANGUAGES.items() %}
|
||||
<option value="{{key}}" {% if user_language == key %}selected{% endif %}>{{lang}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</select>
|
||||
</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>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -176,7 +176,7 @@ def current_user_info():
|
||||
config.ALLOW_SAVE_TUNNEL_PASSWORD and session[
|
||||
'allow_save_password'] else 'false',
|
||||
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
|
||||
),
|
||||
status=200,
|
||||
|
@ -28,6 +28,7 @@ define([
|
||||
DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'],
|
||||
LDAP = pgConst['LDAP'],
|
||||
KERBEROS = pgConst['KERBEROS'],
|
||||
OAUTH2 = pgConst['OAUTH2'],
|
||||
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false,
|
||||
userFilter = function(collection) {
|
||||
return (new Backgrid.Extension.ClientSideFilter({
|
||||
@ -607,6 +608,16 @@ define([
|
||||
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);
|
||||
return errmsg;
|
||||
}
|
||||
@ -1053,7 +1064,7 @@ define([
|
||||
saveUser: function(m) {
|
||||
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.get('newPassword') || !m.get('confirmPassword') || 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'
|
||||
LDAP = 'ldap'
|
||||
KERBEROS = 'kerberos'
|
||||
OAUTH2 = "oauth2"
|
||||
|
||||
SUPPORTED_AUTH_SOURCES = [INTERNAL,
|
||||
LDAP,
|
||||
KERBEROS]
|
||||
KERBEROS,
|
||||
OAUTH2]
|
||||
|
||||
BINARY_PATHS = {
|
||||
"as_bin_paths": [
|
||||
|
@ -319,7 +319,7 @@ class Connection(BaseConnection):
|
||||
config.APP_NAME, conn_id)
|
||||
|
||||
if config.SERVER_MODE and \
|
||||
session['_auth_source_manager_obj']['current_source'] == \
|
||||
session['auth_source_manager']['current_source'] == \
|
||||
KERBEROS and 'KRB5CCNAME' in session\
|
||||
and manager.kerberos_conn:
|
||||
lock.acquire()
|
||||
@ -353,7 +353,7 @@ class Connection(BaseConnection):
|
||||
self._wait(pg_conn)
|
||||
|
||||
if config.SERVER_MODE and \
|
||||
session['_auth_source_manager_obj']['current_source'] == \
|
||||
session['auth_source_manager']['current_source'] == \
|
||||
KERBEROS:
|
||||
environ['KRB5CCNAME'] = ''
|
||||
|
||||
@ -378,7 +378,7 @@ class Connection(BaseConnection):
|
||||
return False, msg
|
||||
finally:
|
||||
if config.SERVER_MODE and \
|
||||
session['_auth_source_manager_obj']['current_source'] == \
|
||||
session['auth_source_manager']['current_source'] == \
|
||||
KERBEROS and lock.locked():
|
||||
lock.release()
|
||||
|
||||
|
@ -31,13 +31,11 @@ def get_crypt_key():
|
||||
return True, current_user.password
|
||||
# if desktop mode and master pass enabled
|
||||
elif config.MASTER_PASSWORD_REQUIRED \
|
||||
and not config.SERVER_MODE and enc_key is None:
|
||||
and enc_key is None:
|
||||
return False, None
|
||||
elif config.SERVER_MODE and \
|
||||
session['_auth_source_manager_obj']['current_source']\
|
||||
== KERBEROS:
|
||||
return True, session['kerberos_key'] if 'kerberos_key' in session \
|
||||
else None
|
||||
elif not config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
|
||||
'pass_enc_key' in session:
|
||||
return True, session['pass_enc_key']
|
||||
else:
|
||||
return True, enc_key
|
||||
|
||||
|
@ -92,9 +92,7 @@ class TestClient(testing.FlaskClient):
|
||||
# and make a test request context that has those cookies in it.
|
||||
environ_overrides = {}
|
||||
self.cookie_jar.inject_wsgi(environ_overrides)
|
||||
with self.app.test_request_context(
|
||||
"/login", environ_overrides=environ_overrides,
|
||||
):
|
||||
with self.app.test_request_context():
|
||||
# Now, we call Flask-WTF's method of generating a CSRF token...
|
||||
csrf_token = generate_csrf()
|
||||
# ...which also sets a value in `flask.session`, so we need to
|
||||
@ -106,18 +104,27 @@ class TestClient(testing.FlaskClient):
|
||||
return csrf_token
|
||||
|
||||
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:
|
||||
res = self.get('/login', follow_redirects=True)
|
||||
res = self.get('/login',
|
||||
follow_redirects=_follow_redirects)
|
||||
csrf_token = self.fetch_csrf(res)
|
||||
else:
|
||||
|
||||
if csrf_token is None:
|
||||
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(
|
||||
'/authenticate/login', data=dict(
|
||||
email=email, password=password,
|
||||
csrf_token=csrf_token,
|
||||
),
|
||||
'/authenticate/login', data=form_data,
|
||||
follow_redirects=_follow_redirects,
|
||||
headers=headers
|
||||
)
|
||||
|
@ -1656,14 +1656,14 @@ def create_user(user_details):
|
||||
cur = conn.cursor()
|
||||
user_details = (
|
||||
user_details['login_username'], user_details['login_username'],
|
||||
user_details['login_password'], 1)
|
||||
user_details['login_password'], 1, uuid.uuid4().hex)
|
||||
|
||||
cur.execute(
|
||||
'select * from user where username = "%s"' % user_details[0])
|
||||
user = cur.fetchone()
|
||||
if user is None:
|
||||
cur.execute('INSERT INTO user (username, email, password, active) '
|
||||
'VALUES (?,?,?,?)', user_details)
|
||||
cur.execute('INSERT INTO user (username, email, password, active,'
|
||||
' fs_uniquifier) VALUES (?,?,?,?,?)', user_details)
|
||||
user_id = cur.lastrowid
|
||||
conn.commit()
|
||||
else:
|
||||
|
@ -97,6 +97,7 @@ from regression.python_test_utils import test_utils
|
||||
from regression.python_test_utils.csrf_test_client import TestClient
|
||||
|
||||
config.SETTINGS_SCHEMA_VERSION = SCHEMA_VERSION
|
||||
from pgadmin.utils.constants import LDAP
|
||||
|
||||
# Override some other defaults
|
||||
from logging import WARNING
|
||||
@ -117,7 +118,7 @@ if config.SERVER_MODE is True:
|
||||
app.config['WTF_CSRF_ENABLED'] = True
|
||||
|
||||
# Authentication sources
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = LDAP
|
||||
|
||||
app.test_client_class = TestClient
|
||||
test_client = app.test_client()
|
||||
|
@ -9016,9 +9016,9 @@ watchpack@^2.0.0:
|
||||
glob-to-regexp "^0.4.1"
|
||||
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"
|
||||
resolved "git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f"
|
||||
resolved "git+https://github.com/EnterpriseDB/wcDocker/#06daee1a8111b4ed08d58670674a2967e4f68313"
|
||||
dependencies:
|
||||
"@fortawesome/fontawesome-free" "^5.14.0"
|
||||
FileSaver "^0.10.0"
|
||||
|
Loading…
Reference in New Issue
Block a user