This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/endpoints/oauth/login.py
Joseph Schorr 1bd4422da9 Move auth decorators into a decorators module
The non-decorators will be broken out in the followup change
2017-03-23 15:42:45 -04:00

231 lines
9.2 KiB
Python

import logging
from collections import namedtuple
from flask import request, redirect, url_for, Blueprint
from peewee import IntegrityError
import features
from app import app, analytics, get_app_url, oauth_login, authentication
from auth.auth_context import get_authenticated_user
from auth.decorators import require_session_login
from data import model
from endpoints.common import common_login
from endpoints.web import index
from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME
from oauth.login import OAuthLoginException
from util.validation import generate_valid_usernames
logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT']
oauthlogin = Blueprint('oauthlogin', __name__)
oauthlogin_csrf_protect = csrf_protect(OAUTH_CSRF_TOKEN_NAME, 'state', all_methods=True)
OAuthResult = namedtuple('oauthresult', ['user_obj', 'service_name', 'error_message',
'register_redirect'])
def _oauthresult(user_obj=None, service_name=None, error_message=None, register_redirect=False):
return OAuthResult(user_obj, service_name, error_message, register_redirect)
def _get_response(result):
if result.error_message is not None:
return _render_ologin_error(result.service_name, result.error_message, result.register_redirect)
return _perform_login(result.user_obj, result.service_name)
def _conduct_oauth_login(auth_system, login_service, lid, lusername, lemail, metadata=None):
""" Conducts login from the result of an OAuth service's login flow and returns
the status of the login, as well as the followup step. """
service_id = login_service.service_id()
service_name = login_service.service_name()
# Check for an existing account *bound to this service*. If found, conduct login of that account
# and redirect.
user_obj = model.user.verify_federated_login(service_id, lid)
if user_obj is not None:
return _oauthresult(user_obj=user_obj, service_name=service_name)
# If the login service has a bound field name, and we have a defined internal auth type that is
# not the database, then search for an existing account with that matching field. This allows
# users to setup SSO while also being backed by something like LDAP.
bound_field_name = login_service.login_binding_field()
if auth_system.federated_service is not None and bound_field_name is not None:
# Perform lookup.
logger.debug('Got oauth bind field name of "%s"', bound_field_name)
lookup_value = None
if bound_field_name == 'sub':
lookup_value = lid
elif bound_field_name == 'username':
lookup_value = lusername
elif bound_field_name == 'email':
lookup_value = lemail
if lookup_value is None:
logger.error('Missing lookup value for OAuth login')
return _oauthresult(service_name=service_name,
error_message='Configuration error in this provider')
(user_obj, err) = auth_system.link_user(lookup_value)
if err is not None:
logger.debug('%s %s not found: %s', bound_field_name, lookup_value, err)
msg = '%s %s not found in backing auth system' % (bound_field_name, lookup_value)
return _oauthresult(service_name=service_name, error_message=msg)
# Found an existing user. Bind their internal auth account to this service as well.
result = _attach_service(login_service, user_obj, lid, lusername)
if result.error_message is not None:
return result
return _oauthresult(user_obj=user_obj, service_name=service_name)
# Otherwise, we need to create a new user account.
if not features.USER_CREATION:
error_message = 'User creation is disabled. Please contact your administrator'
return _oauthresult(service_name=service_name, error_message=error_message)
# Try to create the user
try:
# Generate a valid username.
new_username = None
for valid in generate_valid_usernames(lusername):
if model.user.get_user_or_org(valid):
continue
new_username = valid
break
prompts = model.user.get_default_user_prompts(features)
user_obj = model.user.create_federated_user(new_username, lemail, service_id, lid,
set_password_notification=True,
metadata=metadata or {},
prompts=prompts,
email_required=features.MAILING)
# Success, tell analytics
analytics.track(user_obj.username, 'register', {'service': service_name.lower()})
return _oauthresult(user_obj=user_obj, service_name=service_name)
except model.InvalidEmailAddressException:
message = ("The e-mail address {0} is already associated "
"with an existing {1} account. \n"
"Please log in with your username and password and "
"associate your {2} account to use it in the future.")
message = message.format(lemail, app.config['REGISTRY_TITLE_SHORT'], service_name)
return _oauthresult(service_name=service_name, error_message=message,
register_redirect=True)
except model.DataModelException as ex:
return _oauthresult(service_name=service_name, error_message=ex.message)
def _render_ologin_error(service_name, error_message=None, register_redirect=False):
""" Returns a Flask response indicating an OAuth error. """
user_creation = bool(features.USER_CREATION and features.DIRECT_LOGIN)
error_info = {
'reason': 'ologinerror',
'service_name': service_name,
'error_message': error_message or 'Could not load user data. The token may have expired',
'service_url': get_app_url(),
'user_creation': user_creation,
'register_redirect': register_redirect,
}
resp = index('', error_info=error_info)
resp.status_code = 400
return resp
def _perform_login(user_obj, service_name):
""" Attempts to login the given user, returning the Flask result of whether the login succeeded.
"""
if common_login(user_obj):
if model.user.has_user_prompts(user_obj):
return redirect(url_for('web.updateuser'))
else:
return redirect(url_for('web.index'))
else:
return _render_ologin_error(service_name, 'Could not login. Account may be disabled')
def _attach_service(login_service, user_obj, lid, lusername):
""" Attaches the given user account to the given service, with the given service user ID and
service username.
"""
metadata = {
'service_username': lusername,
}
try:
model.user.attach_federated_login(user_obj, login_service.service_id(), lid,
metadata=metadata)
return _oauthresult(user_obj=user_obj)
except IntegrityError:
err = '%s account %s is already attached to a %s account' % (
login_service.service_name(), lusername, app.config['REGISTRY_TITLE_SHORT'])
return _oauthresult(service_name=login_service.service_name(), error_message=err)
def _register_service(login_service):
""" Registers the given login service, adding its callback and attach routes to the blueprint. """
@oauthlogin_csrf_protect
def callback_func():
# Check for a callback error.
error = request.args.get('error', None)
if error:
return _render_ologin_error(login_service.service_name(), error)
# Exchange the OAuth code for login information.
code = request.args.get('code')
try:
lid, lusername, lemail = login_service.exchange_code_for_login(app.config, client, code, '')
except OAuthLoginException as ole:
logger.exception('Got login exception')
return _render_ologin_error(login_service.service_name(), ole.message)
# Conduct login.
metadata = {
'service_username': lusername,
}
result = _conduct_oauth_login(authentication, login_service, lid, lusername, lemail,
metadata=metadata)
return _get_response(result)
@require_session_login
@oauthlogin_csrf_protect
def attach_func():
# Check for a callback error.
error = request.args.get('error', None)
if error:
return _render_ologin_error(login_service.service_name(), error)
# Exchange the OAuth code for login information.
code = request.args.get('code')
try:
lid, lusername, _ = login_service.exchange_code_for_login(app.config, client, code, '/attach')
except OAuthLoginException as ole:
return _render_ologin_error(login_service.service_name(), ole.message)
# Conduct attach.
user_obj = get_authenticated_user()
result = _attach_service(login_service, user_obj, lid, lusername)
if result.error_message is not None:
return _get_response(result)
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
oauthlogin.add_url_rule('/%s/callback' % login_service.service_id(),
'%s_oauth_callback' % login_service.service_id(),
callback_func,
methods=['GET'])
oauthlogin.add_url_rule('/%s/callback/attach' % login_service.service_id(),
'%s_oauth_attach' % login_service.service_id(),
attach_func,
methods=['GET'])
# Register the routes for each of the login services.
for current_service in oauth_login.services:
_register_service(current_service)