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
+53 -134
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
)