Added support for OAuth 2 authentication. Fixes #5940

Initial patch sent by: Florian Sabonchi
This commit is contained in:
Khushboo Vashi 2021-07-06 13:22:58 +05:30 committed by Akshay Joshi
parent fff4060b31
commit 48ca83f31d
35 changed files with 750 additions and 227 deletions

View File

@ -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.

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -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
View 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

View File

@ -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

View File

@ -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.*

View File

@ -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
##########################################################################

View File

@ -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()

View File

@ -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
)

View File

@ -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()

View File

@ -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,

View 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']}

View File

@ -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){

View File

@ -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))

View File

@ -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):

View File

@ -12,6 +12,7 @@ define('pgadmin.browser.constants', [], function() {
return {
'INTERNAL': '{{ INTERNAL }}',
'LDAP': '{{ LDAP }}',
'KERBEROS': '{{ KERBEROS }}'
'KERBEROS': '{{ KERBEROS }}',
'OAUTH2': '{{ OAUTH2 }}'
}
});

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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."""

View File

@ -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."""

View 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)

View File

@ -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']

View File

@ -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;

View File

@ -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 %}

View File

@ -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,

View File

@ -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'))) {

View File

@ -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": [

View File

@ -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()

View File

@ -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

View File

@ -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
)

View File

@ -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:

View File

@ -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()

View File

@ -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"