class AlreadyLinkedError(Exception):
    """Signifies that an account was already linked to another account."""

    def __init__(self, user, external_id):
        """Initialize exception."""
        self.user = user
        self.external_id = external_id class OAuthClientAlreadyAuthorized(Exception):
    """Define exception for user already authorized.""" class OAuthClientError(OAuthResponseError):
    """Define OAuth client exception.

    Client errors happens when the client (i.e. Invenio) creates an invalid
    request.
    """

    def __init__(self, message, remote, response):
        """Initialize exception.

        :param message: Error message.
        :param remote: Remote application.
        :param response: OAuth response object. Used to extract ``error``,
                         ``error_uri`` and ``error_description``.
        """
        # Only OAuth2 specifies how to send error messages
        self.code = response['error']
        self.uri = response.get('error_uri', None)
        self.description = response.get('error_description', None)
        super(OAuthClientError, self).__init__(
            self.description or message, remote, response
        ) class OAuthClientMustRedirectLogin(Exception):
    """Define exception for forcing redirection to login view.""" class OAuthClientMustRedirectSignup(Exception):
    """Define exception for forcing redirection to signup view.""" class OAuthClientTokenNotFound(Exception):
    """Define exception for oauth token not found.""" class OAuthClientTokenNotSet(Exception):
    """Define exception for oauth token not set.""" class OAuthClientUnAuthorized(Exception):
    """Define exception for unauthorized user.""" class OAuthClientUserNotRegistered(Exception):
    """Define exception of user not registered.""" class OAuthError(Exception):
    """Base class for OAuth exceptions."""

    def __init__(self, message, remote):
        """Initialize exception.

        :param message: Error message.
        :param message: Remote application.
        """
        self.message = message
        self.remote = remote class OAuthRejectedRequestError(OAuthResponseError):
    """Define exception of rejected response during OAuth process.""" def create_csrf_disabled_registrationform(remote):
    """Create a registration form with CSRF disabled."""
    func = current_app.config["OAUTHCLIENT_SIGNUP_FORM"]
    return func(oauth_remote_app=remote, **_get_csrf_disabled_param()) def fill_form(form, data):
    """Prefill form with data.

    :param form: The form to fill.
    :param data: The data to insert in the form.
    :returns: A pre-filled form.
    """
    for (key, value) in data.items():
        if hasattr(form, key):
            field = getattr(form, key)
            if isinstance(value, dict):
                fill_form(field, value)
            elif field is not None:
                field.data = value
    return form def base_authorized_signup_handler(resp, remote, *args, **kwargs):
    """Handle sign-in/up functionality.

    :param remote: The remote application.
    :param resp: The response.
    :returns: Redirect response.
    """
    # Remove any previously stored auto register session key
    session.pop(token_session_key(remote.name) + '_autoregister', None)

    # Store token in session
    # ----------------------
    # Set token in session - token object only returned if
    # current_user.is_autenticated().
    token = response_token_setter(remote, resp)
    handlers = current_oauthclient.signup_handlers[remote.name]

    # Sign-in/up user
    # ---------------
    if not current_user.is_authenticated:
        account_info = handlers['info'](resp)
        account_info_received.send(
            remote, token=token, response=resp, account_info=account_info
        )

        user = oauth_get_user(
            remote.consumer_key,
            account_info=account_info,
            access_token=token_getter(remote)[0],
        )

        if user is None:
            # Auto sign-up if user not found
            form = create_csrf_disabled_registrationform(remote)
            form = fill_form(
                form,
                account_info['user']
            )
            remote_apps = current_app.config['OAUTHCLIENT_REMOTE_APPS']
            precedence_mask = remote_apps[remote.name].get("precedence_mask")
            user = oauth_register(form, account_info['user'], precedence_mask)

            # if registration fails ...
            if user is None:
                # requires extra information
                session[
                    token_session_key(remote.name) + '_autoregister'] = True
                session[token_session_key(remote.name) +
                        '_account_info'] = account_info
                session[token_session_key(remote.name) +
                        '_response'] = resp
                db.session.commit()
                raise OAuthClientMustRedirectSignup()

        # Authenticate user
        if not oauth_authenticate(remote.consumer_key, user,
                                  require_existing_link=False):
            raise OAuthClientUnAuthorized()

        # Link account
        # ------------
        # Need to store token in database instead of only the session when
        # called first time.
        token = response_token_setter(remote, resp)

    # Setup account
    # -------------
    if not token.remote_account.extra_data:
        account_setup = handlers['setup'](token, resp)
        account_setup_received.send(
            remote, token=token, response=resp, account_setup=account_setup
        )
        db.session.commit()
        account_setup_committed.send(remote, token=token)
    else:
        db.session.commit()

    # Redirect to next
    next_url = get_session_next_url(remote.name)
    if next_url:
        return next_url @require_more_than_one_external_account
def base_disconnect_handler(remote, *args, **kwargs):
    """Handle unlinking of remote account.

    This default handler will just delete the remote account link. You may
    wish to extend this module to perform clean-up in the remote service
    before removing the link (e.g. removing install webhooks).

    :param remote: The remote application.
    :returns: Redirect response.
    """
    if not current_user.is_authenticated:
        raise OAuthClientUnAuthorized()

    with db.session.begin_nested():
        account = RemoteAccount.get(
            user_id=current_user.get_id(),
            client_id=remote.consumer_key
        )
        if account:
            account.delete()

    db.session.commit() def base_signup_handler(remote, form, *args, **kwargs):
    """Handle extra signup information.

    :param remote: The remote application.
    :returns: Redirect response or the template rendered.
    """
    # User already authenticated so move on
    if current_user.is_authenticated:
        raise OAuthClientAlreadyAuthorized()

    # Retrieve token from session
    oauth_token = token_getter(remote)
    if not oauth_token:
        raise OAuthClientTokenNotFound()

    session_prefix = token_session_key(remote.name)

    # Test to see if this is coming from on authorized request
    if not session.get(session_prefix + '_autoregister', False):
        raise OAuthClientMustRedirectLogin()

    handlers = current_oauthclient.signup_handlers[remote.name]
    if form.validate_on_submit():
        account_info = session.get(session_prefix + '_account_info')
        response = session.get(session_prefix + '_response')
        remote_apps = current_app.config['OAUTHCLIENT_REMOTE_APPS']
        precedence_mask = remote_apps[remote.name].get("precedence_mask")

        # Register user
        user = oauth_register(form, account_info['user'], precedence_mask)

        if user is None:
            raise OAuthClientUserNotRegistered()

        # Remove session key
        session.pop(session_prefix + '_autoregister', None)

        # Link account and set session data
        token = token_setter(remote, oauth_token[0], secret=oauth_token[1],
                             user=user)
        handlers = current_oauthclient.signup_handlers[remote.name]

        if token is None:
            raise OAuthClientTokenNotSet()

        if not token.remote_account.extra_data:
            account_setup = handlers['setup'](token, response)
            account_setup_received.send(
                remote, token=token, response=response,
                account_setup=account_setup
            )
            # Registration has been finished
            db.session.commit()
            account_setup_committed.send(remote, token=token)
        else:
            # Registration has been finished
            db.session.commit()

        # Authenticate the user
        if not oauth_authenticate(remote.consumer_key, user,
                                  require_existing_link=False):
            raise OAuthClientUnAuthorized()

        # Remove account info from session
        session.pop(session_prefix + '_account_info', None)
        session.pop(session_prefix + '_response', None)

        # Redirect to next
        next_url = get_session_next_url(remote.name)
        if next_url:
            return next_url

    # Pre-fill form
    account_info = session.get(session_prefix + '_account_info')
    if not form.is_submitted():
        form = fill_form(form, account_info['user']) def response_token_setter(remote, resp):
    """Extract token from response and set it for the user.

    :param remote: The remote application.
    :param resp: The response.
    :raises invenio_oauthclient.errors.OAuthClientError: If authorization with
        remote service failed.
    :raises invenio_oauthclient.errors.OAuthResponseError: In case of bad
        authorized request.
    :returns: The token.
    """
    if resp is None:
        raise OAuthRejectedRequestError('User rejected request.', remote, resp)
    else:
        if 'access_token' in resp:
            return oauth2_token_setter(remote, resp)
        elif 'oauth_token' in resp and 'oauth_token_secret' in resp:
            return oauth1_token_setter(remote, resp)
        elif 'error' in resp:
            # Only OAuth2 specifies how to send error messages
            raise OAuthClientError(
                'Authorization with remote service failed.', remote, resp,
            )
    raise OAuthResponseError('Bad OAuth authorized request', remote, resp)
from functools import partial, wraps
from flask import abort, current_app, jsonify, make_response, redirect, \
    render_template, request, url_for
from invenio_db import db
from six.moves.urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
from ..errors import AlreadyLinkedError, OAuthClientAlreadyAuthorized, \
    OAuthClientError, OAuthClientMustRedirectLogin, \
    OAuthClientMustRedirectSignup, OAuthClientTokenNotFound, \
    OAuthClientTokenNotSet, OAuthClientUnAuthorized, \
    OAuthClientUserNotRegistered, OAuthError, OAuthRejectedRequestError
from ..proxies import current_oauthclient
from ..utils import create_csrf_disabled_registrationform, fill_form
from .base import base_authorized_signup_handler, base_disconnect_handler, \
    base_signup_handler
from .utils import response_token_setter
            )
        )
    except OAuthClientTokenNotSet:
        raise OAuthError('Could not create token for user.', remote)
    except OAuthClientMustRedirectSignup as e:
        return redirect(url_for('.rest_signup', remote_app=remote.name,))
    except OAuthClientMustRedirectLogin as e:
        return redirect(url_for('.rest_login', remote_app=remote.name,))


#
# Error handling decorators
#
def oauth_resp_remote_error_handler(f):
    """Decorator to handle exceptions."""
    @wraps(f)
    def inner(resp, remote, *args, **kwargs):
        # OAuthErrors should not happen, so they are not caught here. Hence
        # they will result in a 500 Internal Server Error which is what we
        # are interested in.
        _f = partial(f, resp)
        return _oauth_error_handler(remote, _f, *args, **kwargs)
    return inner


def oauth_remote_error_handler(f):
    """Decorator to handle exceptions."""
    @wraps(f)
    def inner(remote, *args, **kwargs):
        # OAuthErrors should not happen, so they are not caught here. Hence
        # they will result in a 500 Internal Server Error which is what we
        # are interested in.
        return _oauth_error_handler(remote, f, *args, **kwargs)
    return inner


#
# Handlers
#
@oauth_resp_remote_error_handler
def authorized_default_handler(resp, remote, *args, **kwargs):
    """Store access token in session.

    Default authorized handler.

    :param remote: The remote application.
    :param resp: The response.
    :returns: Redirect response.
    """
    remote_app_config = current_app.config['OAUTHCLIENT_REST_REMOTE_APPS'][
        remote.name]

    response_token_setter(remote, resp)
    db.session.commit()
    return response_handler(
        remote,
        remote_app_config['authorized_redirect_url'],
    )


@oauth_resp_remote_error_handler
def authorized_signup_handler(resp, remote, *args, **kwargs):
    """Handle sign-in/up functionality.

    :param remote: The remote application.
    :param resp: The response.
    :returns: Redirect response.
    """
    remote_app_config = current_app.config['OAUTHCLIENT_REST_REMOTE_APPS'][
        remote.name]
    next_url = base_authorized_signup_handler(resp, remote, *args, **kwargs)
    response_payload = dict(message="Successfully authorized.", code=200)
    if next_url:
        response_payload["next_url"] = next_url

    return response_handler(
        remote,
        remote_app_config['authorized_redirect_url'],
        payload=response_payload
    )


@oauth_remote_error_handler
def disconnect_handler(remote, *args, **kwargs):
    """Handle unlinking of remote account.

    This default handler will just delete the remote account link. You may
    wish to extend this module to perform clean-up in the remote service
    before removing the link (e.g. removing install webhooks).

    :param remote: The remote application.
    :returns: Redirect response.
    """
    remote_app_config = current_app.config['OAUTHCLIENT_REST_REMOTE_APPS'][
        remote.name]
    base_disconnect_handler(remote, *args, **kwargs)
    redirect_url = current_app.config['OAUTHCLIENT_REST_REMOTE_APPS'][
        remote.name]['disconnect_redirect_url']

    return response_handler(
        remote,
        redirect_url,
        payload=dict(
            message="Successfully disconnected.",
            code=200,
        )
    )


@oauth_remote_error_handler
def signup_handler(remote, *args, **kwargs):
    """Handle extra signup information.

    :param remote: The remote application.
    :returns: Redirect response or the template rendered.
    """
    remote_app_config = current_app.config['OAUTHCLIENT_REST_REMOTE_APPS'][
        remote.name]
    try:
        form = create_csrf_disabled_registrationform(remote)

        form = fill_form(form, request.json or {})