Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refresh token support #1159

Merged
merged 7 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ By order of apparition, thanks:
* François Freitag (S3)
* Uxío Fuentefría (S3)
* wigeria (Google Cloud Storage patch)
* Alexander Demin (Dropbox fixes)


Extra thanks to Marty for adding this in Django,
Expand Down
61 changes: 61 additions & 0 deletions docs/backends/dropbox.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,29 @@ To use DropBoxStorage set::

DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage'

Two methods of authenticating are supported:

1. using an access token
2. using a refresh token with an app key and secret

Dropbox has recently introduced short-lived access tokens only, and does not seem to allow new apps to generate access tokens that do not expire. Short-lived access tokens can be indentified by their prefix (short-lived access tokens start with ``'sl.'``).

Please set the following variables accordingly:

``DROPBOX_OAUTH2_TOKEN``
Your Dropbox token. You can obtain one by following the instructions in the `tutorial`_.

``DROPBOX_APP_KEY``
Your Dropbox appkey. You can obtain one by following the instructions in the `tutorial`_.

``DROPBOX_APP_SECRET``
Your Dropbox secret. You can obtain one by following the instructions in the `tutorial`_.

``DROPBOX_OAUTH2_REFRESH_TOKEN``
Your Dropbox refresh token. You can obtain one by following the instructions in the `tutorial`_.

The refresh token can be obtained using the `commandline-oauth.py`_ example from the `Dropbox SDK for Python`_.

``DROPBOX_ROOT_PATH`` (optional, default ``'/'``)
Path which will prefix all uploaded files. Must begin with a ``/``.

Expand All @@ -29,6 +49,47 @@ To use DropBoxStorage set::
``DROPBOX_WRITE_MODE`` (optional, default ``'add'``)
Sets the Dropbox WriteMode strategy. Read more in the `official docs`_.

Obtain the refresh token manually
#################################

You can obtail the refresh token manually via ``APP_KEY`` and ``APP_SECRET``.

Get AUTHORIZATION_CODE
**********************

Using your ``APP_KEY`` follow the link:

https://www.dropbox.com/oauth2/authorize?client_id=APP_KEY&token_access_type=offline&response_type=code

It will give you ``AUTHORIZATION_CODE``.

Obtain the refresh token
*************************

Usinh your ``APP_KEY``, ``APP_SECRET`` and ``AUTHORIZATION_KEY`` obtain the refresh token.

.. code-block:: shell

curl -u APP_KEY:APP_SECRET \
-d "code=AUTHORIZATION_CODE&grant_type=authorization_code" \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST "https://api.dropboxapi.com/oauth2/token"

The response would be:

.. code-block:: json

{
"access_token": "sl.************************",
"token_type": "bearer",
"expires_in": 14400,
"refresh_token": "************************", <-- your REFRESH_TOKEN
"scope": <SCOPES>,
"uid": "************************",
"account_id": "dbid:************************"
}

.. _`tutorial`: https://www.dropbox.com/developers/documentation/python#tutorial
.. _`Dropbox SDK for Python`: https://www.dropbox.com/developers/documentation/python#tutorial
.. _`official docs`: https://dropbox-sdk-python.readthedocs.io/en/latest/api/files.html#dropbox.files.WriteMode
.. _`commandline-oauth.py`: https://github.com/dropbox/dropbox-sdk-python/blob/master/example/oauth/commandline-oauth.py
44 changes: 35 additions & 9 deletions storages/backends/dropbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
# Usage:
#
# Add below to settings.py:
# DROPBOX_OAUTH2_TOKEN = 'YourOauthToken'
# DROPBOX_ROOT_PATH = '/dir/'
# DROPBOX_OAUTH2_TOKEN = 'YourOauthToken'
# DROPBOX_APP_KEY = 'YourAppKey'
# DROPBOX_APP_SECRET = 'YourAppSecret``
# DROPBOX_OAUTH2_REFRESH_TOKEN = 'YourOauthRefreshToken'


from io import BytesIO
from shutil import copyfileobj
Expand Down Expand Up @@ -69,21 +73,43 @@ class DropBoxStorage(Storage):
"""DropBox Storage class for Django pluggable storage system."""
location = setting('DROPBOX_ROOT_PATH', '/')
oauth2_access_token = setting('DROPBOX_OAUTH2_TOKEN')
app_key = setting('DROPBOX_APP_KEY')
app_secret = setting('DROPBOX_APP_SECRET')
oauth2_refresh_token = setting('DROPBOX_OAUTH2_REFRESH_TOKEN')
timeout = setting('DROPBOX_TIMEOUT', _DEFAULT_TIMEOUT)
write_mode = setting('DROPBOX_WRITE_MODE', _DEFAULT_MODE)

CHUNK_SIZE = 4 * 1024 * 1024

def __init__(self, oauth2_access_token=oauth2_access_token, root_path=location, timeout=timeout,
write_mode=write_mode):
if oauth2_access_token is None:
raise ImproperlyConfigured("You must configure an auth token at"
"'settings.DROPBOX_OAUTH2_TOKEN'.")
if write_mode not in ["add", "overwrite", "update"]:
raise ImproperlyConfigured("DROPBOX_WRITE_MODE must be set to either: 'add', 'overwrite' or 'update'")
def __init__(
self,
oauth2_access_token=oauth2_access_token,
app_key=app_key,
app_secret=app_secret,
oauth2_refresh_token=oauth2_refresh_token,
root_path=location,
timeout=timeout,
write_mode=write_mode,
):
if oauth2_access_token is None and not all(
[app_key, app_secret, oauth2_refresh_token]
):
raise ImproperlyConfigured(
"You must configure an auth token at"
"'settings.DROPBOX_OAUTH2_TOKEN' or "
"'setting.DROPBOX_APP_KEY', "
"'setting.DROPBOX_APP_SECRET' "
"and 'setting.DROPBOX_OAUTH2_REFRESH_TOKEN'."
)
self.root_path = root_path
self.write_mode = write_mode
self.client = Dropbox(oauth2_access_token, timeout=timeout)
self.client = Dropbox(
oauth2_access_token,
app_key=app_key,
app_secret=app_secret,
oauth2_refresh_token=oauth2_refresh_token,
timeout=timeout,
)

def _full_path(self, name):
if name == '/':
Expand Down
33 changes: 27 additions & 6 deletions tests/test_dropbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
from datetime import datetime
from unittest import mock

from django.core.exceptions import (
ImproperlyConfigured, SuspiciousFileOperation,
)
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import File
from django.test import TestCase
from dropbox.files import FileMetadata, FolderMetadata, GetTemporaryLinkResult
Expand Down Expand Up @@ -54,6 +52,30 @@ def test_no_access_token(self, *args):
with self.assertRaises(ImproperlyConfigured):
dropbox.DropBoxStorage(None)

def test_refresh_token_app_key_no_app_secret(self, *args):
inputs = {
'oauth2_refresh_token': 'foo',
'app_key': 'bar',
}
with self.assertRaises(ImproperlyConfigured):
dropbox.DropBoxStorage(**inputs)

def test_refresh_token_app_secret_no_app_key(self, *args):
inputs = {
'oauth2_refresh_token': 'foo',
'app_secret': 'bar',
}
with self.assertRaises(ImproperlyConfigured):
dropbox.DropBoxStorage(**inputs)

def test_app_key_app_secret_no_refresh_token(self, *args):
inputs = {
'app_key': 'foo',
'app_secret': 'bar',
}
with self.assertRaises(ImproperlyConfigured):
dropbox.DropBoxStorage(**inputs)

@mock.patch('dropbox.Dropbox.files_delete',
return_value=FILE_METADATA_MOCK)
def test_delete(self, *args):
Expand Down Expand Up @@ -168,10 +190,9 @@ def test_jailed(self, *args):
self.assertFalse(dirs)
self.assertFalse(files)

def test_suspicious(self, *args):
def test_relative_path(self, *args):
self.storage = dropbox.DropBoxStorage('foo', '/bar')
with self.assertRaises((SuspiciousFileOperation, ValueError)):
self.storage._full_path('..')
self.assertEqual('/', self.storage._full_path('..'))

def test_formats(self, *args):
self.storage = dropbox.DropBoxStorage('foo', '/bar')
Expand Down