mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-09 04:28:38 -05:00
Added support for OAuth 2 authentication. Fixes #5940
Initial patch sent by: Florian Sabonchi
This commit is contained in:
committed by
Akshay Joshi
parent
fff4060b31
commit
48ca83f31d
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user