Skip to content

Commit

Permalink
Add Digital Ocean as a provider (#330)
Browse files Browse the repository at this point in the history
* Add Digital Ocean as a provider

- add digitalocean.py to configure blueprint for provider
- add tests for Digital Ocean provider
- update CHANGELOG
- update docs/providers.rst
- add DigitalOcean to wordlist

* extend underline in docs

* update formatting and fix spelling
  • Loading branch information
mikeabrahamsen authored Nov 19, 2020
1 parent 808899c commit 35aab0e
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Changelog

`unreleased`_
-------------
nothing yet
Added Digital Ocean pre-set configuration

`3.1.0`_ (2020-10-29)
---------------------
Expand Down
12 changes: 12 additions & 0 deletions docs/providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ Azure
already has the Azure AD authentication token loaded (assuming that the user
has authenticated with Azure AD at some point in the past).

Digital Ocean
-------------
.. module:: flask_dance.contrib.digitalocean

.. autofunction:: make_digitalocean_blueprint

.. data:: digitalocean

A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that
already has the Digital Ocean authentication token loaded (assuming that
the user has authenticated with Digital Ocean at some point in the past).

Discord
-------
.. module:: flask_dance.contrib.discord
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ authentiq
Azure
Discord
Dropbox
DigitalOcean
Facebook
GitHub
GitLab
Expand Down
86 changes: 86 additions & 0 deletions flask_dance/contrib/digitalocean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import unicode_literals

from flask_dance.consumer import OAuth2ConsumerBlueprint
from functools import partial
from flask.globals import LocalProxy, _lookup_app_object

try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack


__maintainer__ = "Michael Abrahamsen <[email protected]>"


def make_digitalocean_blueprint(
client_id=None,
client_secret=None,
scope=None,
redirect_url=None,
redirect_to=None,
login_url=None,
authorized_url=None,
session_class=None,
storage=None,
):
"""
Make a blueprint for authenticating with Digital Ocean using OAuth 2.
This requires a client ID and client secret from Digital Ocean.
You should either pass them to this constructor, or make sure that your
Flask application config defines them, using the variables
:envvar:`DIGITALOCEAN_OAUTH_CLIENT_ID` and
:envvar:`DIGITALOCEAN_OAUTH_CLIENT_SECRET`.
Args:
client_id (str): Client ID for your application on Digital Ocean
client_secret (str): Client secret for your Digital Ocean application
scope (str, optional): comma-separated list of scopes for the OAuth
token.
redirect_url (str): the URL to redirect to after the authentication
dance is complete
redirect_to (str): if ``redirect_url`` is not defined, the name of the
view to redirect to after the authentication dance is complete.
The actual URL will be determined by :func:`flask.url_for`
login_url (str, optional): the URL path for the ``login`` view.
Defaults to ``/digitalocean``
authorized_url (str, optional): the URL path for the ``authorized`` view.
Defaults to ``/digitalocean/authorized``.
session_class (class, optional): The class to use for creating a
Requests session. Defaults to
:class:`~flask_dance.consumer.requests.OAuth2Session`.
storage: A token storage class, or an instance of a token storage
class, to use for this blueprint. Defaults to
:class:`~flask_dance.consumer.storage.session.SessionStorage`.
:rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint`
:returns: A :ref:`blueprint <flask:blueprints>` to attach to your Flask app.
"""
digitalocean_bp = OAuth2ConsumerBlueprint(
"digitalocean",
__name__,
client_id=client_id,
client_secret=client_secret,
scope=scope.replace(",", " ") if scope else None,
base_url="https://cloud.digitalocean.com/v1/oauth",
authorization_url="https://cloud.digitalocean.com/v1/oauth/authorize",
token_url="https://cloud.digitalocean.com/v1/oauth/token",
redirect_url=redirect_url,
redirect_to=redirect_to,
login_url=login_url,
authorized_url=authorized_url,
session_class=session_class,
storage=storage,
)
digitalocean_bp.from_config["client_id"] = "DIGITALOCEAN_OAUTH_CLIENT_ID"
digitalocean_bp.from_config["client_secret"] = "DIGITALOCEAN_OAUTH_CLIENT_SECRET"

@digitalocean_bp.before_app_request
def set_applocal_session():
ctx = stack.top
ctx.digitalocean_oauth = digitalocean_bp.session

return digitalocean_bp


digitalocean = LocalProxy(partial(_lookup_app_object, "digitalocean_oauth"))
97 changes: 97 additions & 0 deletions tests/contrib/test_digitalocean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from __future__ import unicode_literals

import pytest
import responses
from urlobject import URLObject
from flask import Flask
from flask_dance.contrib.digitalocean import make_digitalocean_blueprint, digitalocean
from flask_dance.consumer import OAuth2ConsumerBlueprint
from flask_dance.consumer.storage import MemoryStorage


@pytest.fixture
def make_app():
"A callable to create a Flask app with the digitalocean provider"

def _make_app(*args, **kwargs):
app = Flask(__name__)
app.secret_key = "whatever"
blueprint = make_digitalocean_blueprint(*args, **kwargs)
app.register_blueprint(blueprint)
return app

return _make_app


def test_scope_list_is_valid_with_single_scope():
digitalocean_bp = make_digitalocean_blueprint(
client_id="foobar", client_secret="supersecret", scope="read"
)
assert digitalocean_bp.session.scope == "read"


def test_scope_list_is_converted_to_space_delimited():
digitalocean_bp = make_digitalocean_blueprint(
client_id="foobar", client_secret="supersecret", scope="read,write"
)
assert digitalocean_bp.session.scope == "read write"


def test_blueprint_factory():
digitalocean_bp = make_digitalocean_blueprint(
client_id="foobar", client_secret="supersecret"
)
assert isinstance(digitalocean_bp, OAuth2ConsumerBlueprint)
assert digitalocean_bp.session.client_id == "foobar"
assert digitalocean_bp.client_secret == "supersecret"
assert digitalocean_bp.token_url == "https://cloud.digitalocean.com/v1/oauth/token"
assert (
digitalocean_bp.authorization_url
== "https://cloud.digitalocean.com/v1/oauth/authorize"
)


@responses.activate
def test_load_from_config(make_app):
app = make_app()
app.config["DIGITALOCEAN_OAUTH_CLIENT_ID"] = "foo"
app.config["DIGITALOCEAN_OAUTH_CLIENT_SECRET"] = "bar"

resp = app.test_client().get("/digitalocean")
url = resp.headers["Location"]
client_id = URLObject(url).query.dict.get("client_id")
assert client_id == "foo"


@responses.activate
def test_context_local(make_app):
responses.add(responses.GET, "https://google.com")
# set up two apps with two different set of auth tokens
app1 = make_app(
"foo1",
"bar1",
redirect_to="url1",
storage=MemoryStorage({"access_token": "app1"}),
)
app2 = make_app(
"foo2",
"bar2",
redirect_to="url2",
storage=MemoryStorage({"access_token": "app2"}),
)
# outside of a request context, referencing functions on the `digitalocean`
# object will raise an exception
with pytest.raises(RuntimeError):
digitalocean.get("https://google.com")
# inside of a request context, `digitalocean` should be a proxy to the
# correct blueprint session
with app1.test_request_context("/"):
app1.preprocess_request()
digitalocean.get("https://google.com")
request = responses.calls[0].request
assert request.headers["Authorization"] == "Bearer app1"
with app2.test_request_context("/"):
app2.preprocess_request()
digitalocean.get("https://google.com")
request = responses.calls[1].request
assert request.headers["Authorization"] == "Bearer app2"

0 comments on commit 35aab0e

Please sign in to comment.