Skip to content

Commit

Permalink
global: new configurable OAuthRemoteApp factory
Browse files Browse the repository at this point in the history
* NEW Enables to create custom OAuthRemoteApp instances by replacing
  the default remote application factory. (closes #45)

Signed-off-by: Nicolas Harraudeau <nicolas.harraudeau@cern.ch>
  • Loading branch information
Nicolas Harraudeau committed Apr 28, 2016
1 parent 3e5dfff commit d033b02
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 6 deletions.
36 changes: 35 additions & 1 deletion invenio_oauthclient/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2014, 2015 CERN.
# Copyright (C) 2014, 2015, 2016 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
Expand All @@ -26,6 +26,7 @@
an access token. **Default:** ``oauth_token``.
`OAUTHCLIENT_STATE_EXPIRES` Number of seconds after which the state token
expires. Defaults to 300 seconds.
`OAUTHCLIENT_REMOTE_APP` Replaces the default remote application class.
================================ ==============================================
Each remote application must be defined in the ``OAUTHCLIENT_REMOTE_APPS``
Expand Down Expand Up @@ -174,6 +175,39 @@
# ...
)
)
Custom remote application
^^^^^^^^^^^^^^^^^^^^^^^^^
Some OAuth services require a specific handling of OAuth requests. If the
standard flask-oauthlib.client.OAuthRemoteApp does not support it, it is
possible to replace the standard OAuthRemoteApp for all remote application
by referring to the custom class with the configuration variable
``OAUTHCLIENT_REMOTE_APP`` or for only one remote application by
setting ``remote_app`` in your remote application configuration.
.. code-block:: python
class CustomOAuthRemoteApp(OAuthRemoteApp):
pass
app.config.update(
OAUTHCLIENT_REMOTE_APP=
'myproject.mymodule:CustomOAuthRemoteApp'
)
# OR
app.config.update(
OAUTHCLIENT_REMOTE_APPS=dict(
custom_app=dict(
# ...
remote_app=
'myproject.mymodule:CustomOAuthRemoteApp'
)
)
)
"""

OAUTHCLIENT_REMOTE_APPS = {}
Expand Down
19 changes: 16 additions & 3 deletions invenio_oauthclient/ext.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 CERN.
# Copyright (C) 2015, 2016 CERN.
#
# Invenio is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
Expand All @@ -27,11 +27,12 @@
from __future__ import absolute_import, print_function

from flask_login import user_logged_out
from flask_oauthlib.client import OAuth as FlaskOAuth
from flask_oauthlib.client import OAuth as FlaskOAuth, OAuthRemoteApp

from . import config
from .handlers import authorized_default_handler, disconnect_handler, \
make_handler, make_token_getter, oauth_logout_handler
from .utils import load_or_import_from_config, obj_or_import_string


class _OAuthClientState(object):
Expand All @@ -52,11 +53,23 @@ def __init__(self, app):
# Add remote applications
self.oauth.init_app(app)

remote_app_class = load_or_import_from_config(
'OAUTHCLIENT_REMOTE_APP', app, default=OAuthRemoteApp
)

for remote_app, conf in app.config[
'OAUTHCLIENT_REMOTE_APPS'].items():
# Prevent double creation problems
if remote_app not in self.oauth.remote_apps:
remote = self.oauth.remote_app(
# use this app's specific remote app class if there is one.
current_remote_app_class = obj_or_import_string(
conf.get('remote_app'), default=remote_app_class
)
# Register the remote app. We are doing this because the
# current version of OAuth.remote_app does not allow to specify
# the remote app class. Use it once it is fixed.
self.oauth.remote_apps[remote_app] = current_remote_app_class(
self.oauth,
remote_app,
**conf['params']
)
Expand Down
18 changes: 18 additions & 0 deletions invenio_oauthclient/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

"""Utility methods to help find, authenticate or register a remote user."""

import six
from flask import after_this_request, current_app, request
from flask_security import login_user, logout_user
from flask_security.confirmable import requires_confirmation
Expand All @@ -27,6 +28,7 @@
from invenio_db import db
from uritools import urisplit
from werkzeug.local import LocalProxy
from werkzeug.utils import import_string

from .models import RemoteAccount, RemoteToken, UserIdentity

Expand Down Expand Up @@ -131,3 +133,19 @@ def get_safe_redirect_target(arg='next'):
if target and is_local_url(target):
return target
return None


def obj_or_import_string(value, default=None):
"""Import string or return object."""
if isinstance(value, six.string_types):
return import_string(value)
elif value:
return value
return default


def load_or_import_from_config(key, app=None, default=None):
"""Load or import value from config."""
app = app or current_app
imp = app.config.get(key)
return obj_or_import_string(imp, default=default)
58 changes: 56 additions & 2 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@
from __future__ import absolute_import

import os
from copy import deepcopy

from flask import Flask
from flask_cli import FlaskCLI
from flask_oauthlib.client import OAuth as FlaskOAuth
from flask_oauthlib.client import OAuthRemoteApp
from invenio_db import InvenioDB, db
from sqlalchemy_utils.functions import create_database, database_exists, \
drop_database
from sqlalchemy_utils.functions import create_database, database_exists

from invenio_oauthclient import InvenioOAuthClient
from invenio_oauthclient.contrib.orcid import REMOTE_APP


def test_version():
Expand Down Expand Up @@ -61,6 +63,58 @@ def test_init():
assert 'invenio-oauthclient' in app.extensions


class _CustomOAuthRemoteApp(OAuthRemoteApp):
"""Custom OAuthRemoteApp used for testing."""


def test_standard_remote_app_factory(base_app):
"""Test standard remote_app class."""
base_app.config.update(
OAUTHCLIENT_REMOTE_APPS=dict(
custom_app=REMOTE_APP
)
)
FlaskOAuth(base_app)
InvenioOAuthClient(base_app)
assert isinstance(
base_app.extensions['oauthlib.client'].remote_apps['custom_app'],
OAuthRemoteApp)
assert not isinstance(
base_app.extensions['oauthlib.client'].remote_apps['custom_app'],
_CustomOAuthRemoteApp)


def test_remote_app_factory_global_customization(base_app):
"""Test remote_app override with global variable."""
base_app.config.update(
OAUTHCLIENT_REMOTE_APP=_CustomOAuthRemoteApp,
OAUTHCLIENT_REMOTE_APPS=dict(
custom_app=REMOTE_APP
)
)
FlaskOAuth(base_app)
InvenioOAuthClient(base_app)
assert isinstance(
base_app.extensions['oauthlib.client'].remote_apps['custom_app'],
_CustomOAuthRemoteApp)


def test_remote_app_factory_local_customization(base_app):
"""Test custom remote_app for one app only."""
config_for_one_app = deepcopy(REMOTE_APP)
config_for_one_app['remote_app'] = _CustomOAuthRemoteApp
base_app.config.update(
OAUTHCLIENT_REMOTE_APPS=dict(
custom_app=config_for_one_app
)
)
FlaskOAuth(base_app)
InvenioOAuthClient(base_app)
assert isinstance(
base_app.extensions['oauthlib.client'].remote_apps['custom_app'],
_CustomOAuthRemoteApp)


def test_db(request):
"""Test database backend."""
app = Flask(__name__)
Expand Down

0 comments on commit d033b02

Please sign in to comment.