Skip to content

Commit b35881f

Browse files
committed
Add tests for new auth and logging classes.
Add a new coop auth demo.
1 parent 2bb05e3 commit b35881f

19 files changed

+354
-27
lines changed

README.rst

+1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ Other Auth Options
287287

288288
For advanced uses of the SDK, two additional auth classes are provided:
289289

290+
- `CooperativelyManagedOAuth2`: Allows multiple auth instances to share tokens.
290291
- `RemoteOAuth2`: Allows use of the SDK on clients without access to your application's client secret. Instead, you
291292
provide a `retrieve_access_token` callback. That callback should perform the token refresh, perhaps on your server
292293
that does have access to the client secret.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding: utf-8
2+
3+
from __future__ import unicode_literals
4+
from boxsdk import OAuth2
5+
6+
7+
class CooperativelyManagedOAuth2Mixin(OAuth2):
8+
"""
9+
Box SDK OAuth2 mixin.
10+
Allows for sharing auth tokens between multiple clients.
11+
"""
12+
def __init__(self, retrieve_tokens=None, *args, **kwargs):
13+
"""
14+
:param retrieve_tokens:
15+
Callback to get the current access/refresh token pair.
16+
:type retrieve_tokens:
17+
`callable` of () => (`unicode`, `unicode`)
18+
"""
19+
self._retrieve_tokens = retrieve_tokens
20+
super(CooperativelyManagedOAuth2Mixin, self).__init__(*args, **kwargs)
21+
22+
def _get_tokens(self):
23+
"""
24+
Base class override. Get the tokens from the user-specified callback.
25+
"""
26+
return self._retrieve_tokens()
27+
28+
29+
class CooperativelyManagedOAuth2(CooperativelyManagedOAuth2Mixin):
30+
"""
31+
Box SDK OAuth2 subclass.
32+
Allows for sharing auth tokens between multiple clients. The retrieve_tokens callback should
33+
return the current access/refresh token pair.
34+
"""
35+
pass

boxsdk/auth/oauth2.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(
2828
access_token=None,
2929
refresh_token=None,
3030
network_layer=None,
31+
refresh_lock=None,
3132
):
3233
"""
3334
:param client_id:
@@ -62,14 +63,18 @@ def __init__(
6263
If specified, use it to make network requests. If not, the default network implementation will be used.
6364
:type network_layer:
6465
:class:`Network`
66+
:param refresh_lock:
67+
Lock used to synchronize token refresh. If not specified, then a :class:`threading.Lock` will be used.
68+
:type refresh_lock:
69+
Context Manager
6570
"""
6671
self._client_id = client_id
6772
self._client_secret = client_secret
6873
self._store_tokens_callback = store_tokens
6974
self._access_token = access_token
7075
self._refresh_token = refresh_token
7176
self._network_layer = network_layer if network_layer else DefaultNetwork()
72-
self._refresh_lock = Lock()
77+
self._refresh_lock = refresh_lock or Lock()
7378
self._box_device_id = box_device_id
7479
self._box_device_name = box_device_name
7580

boxsdk/auth/redis_managed_oauth2.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ class RedisManagedOAuth2Mixin(OAuth2):
2424
def __init__(self, unique_id=uuid4(), redis_server=None, *args, **kwargs):
2525
self._unique_id = unique_id
2626
self._redis_server = redis_server or StrictRedis()
27-
super(RedisManagedOAuth2Mixin, self).__init__(*args, **kwargs)
27+
refresh_lock = Lock(redis=self._redis_server, name='{0}_lock'.format(self._unique_id))
28+
super(RedisManagedOAuth2Mixin, self).__init__(*args, refresh_lock=refresh_lock, **kwargs)
2829
if self._access_token is None:
2930
self._update_current_tokens()
30-
self._refresh_lock = Lock(redis=self._redis_server, name='{0}_lock'.format(self._unique_id))
3131

3232
def _update_current_tokens(self):
33+
"""
34+
Get the latest tokens from redis and store them.
35+
"""
3336
self._access_token, self._refresh_token = self._redis_server.hvals(self._unique_id) or (None, None)
3437

3538
@property

boxsdk/auth/remote_oauth2.py boxsdk/auth/remote_managed_oauth2.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66

77
class RemoteOAuth2Mixin(OAuth2):
88
"""
9-
Box SDK OAuth2 subclass.
9+
Box SDK OAuth2 mixin.
1010
Allows for storing auth tokens remotely.
1111
12-
:param retrieve_access_token:
13-
Callback to exchange an existing access token for a new one.
14-
:type retrieve_access_token:
15-
`callable` of `unicode` => `unicode`
1612
"""
1713
def __init__(self, retrieve_access_token=None, *args, **kwargs):
14+
"""
15+
:param retrieve_access_token:
16+
Callback to exchange an existing access token for a new one.
17+
:type retrieve_access_token:
18+
`callable` of `unicode` => `unicode`
19+
"""
1820
self._retrieve_access_token = retrieve_access_token
1921
super(RemoteOAuth2Mixin, self).__init__(*args, **kwargs)
2022

@@ -24,3 +26,12 @@ def _refresh(self, access_token):
2426
"""
2527
self._access_token = self._retrieve_access_token(access_token)
2628
return self._access_token, None
29+
30+
31+
class RemoteOAuth2(RemoteOAuth2Mixin):
32+
"""
33+
Box SDK OAuth2 subclass.
34+
Allows for storing auth tokens remotely. The retrieve_access_token callback should
35+
return an access token, presumably acquired from a remote server on which your auth credentials are available.
36+
"""
37+
pass

boxsdk/network/logging_network.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ class LoggingNetwork(DefaultNetwork):
1010
"""
1111
SDK Network subclass that logs requests and responses.
1212
"""
13+
LOGGER_NAME = 'boxsdk.network'
14+
REQUEST_FORMAT = '\x1b[36m%s %s %s\x1b[0m'
15+
SUCCESSFUL_RESPONSE_FORMAT = '\x1b[32m%s\x1b[0m'
16+
ERROR_RESPONSE_FORMAT = '\x1b[31m%s\n%s\n%s\n\x1b[0m'
17+
1318
def __init__(self, logger=None):
1419
"""
1520
:param logger:
@@ -19,7 +24,7 @@ def __init__(self, logger=None):
1924
:class:`Logger`
2025
"""
2126
super(LoggingNetwork, self).__init__()
22-
self._logger = logger or setup_logging(name='network')
27+
self._logger = logger or setup_logging(name=self.LOGGER_NAME)
2328

2429
@property
2530
def logger(self):
@@ -42,7 +47,7 @@ def _log_request(self, method, url, **kwargs):
4247
:type access_token:
4348
`unicode`
4449
"""
45-
self._logger.info('\x1b[36m%s %s %s\x1b[0m', method, url, pformat(kwargs))
50+
self._logger.info(self.REQUEST_FORMAT, method, url, pformat(kwargs))
4651

4752
def _log_response(self, response):
4853
"""
@@ -51,10 +56,10 @@ def _log_response(self, response):
5156
:param response: The Box API response.
5257
"""
5358
if response.ok:
54-
self._logger.info('\x1b[32m%s\x1b[0m', response.content)
59+
self._logger.info(self.SUCCESSFUL_RESPONSE_FORMAT, response.content)
5560
else:
5661
self._logger.warning(
57-
'\x1b[31m%s\n%s\n%s\n\x1b[0m',
62+
self.ERROR_RESPONSE_FORMAT,
5863
response.status_code,
5964
response.headers,
6065
pformat(response.content),

demo/auth.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
CLIENT_SECRET = '' # Insert Box client secret here
1616

1717

18-
def authenticate():
18+
def authenticate(oauth_class=OAuth2):
1919
class StoppableWSGIServer(bottle.ServerAdapter):
2020
def __init__(self, *args, **kwargs):
2121
super(StoppableWSGIServer, self).__init__(*args, **kwargs)
@@ -45,7 +45,7 @@ def get_token():
4545
server_thread = Thread(target=lambda: local_oauth_redirect.run(server=local_server))
4646
server_thread.start()
4747

48-
oauth = OAuth2(
48+
oauth = oauth_class(
4949
client_id=CLIENT_ID,
5050
client_secret=CLIENT_SECRET,
5151
)
@@ -60,7 +60,7 @@ def get_token():
6060
print('access_token: ' + access_token)
6161
print('refresh_token: ' + refresh_token)
6262

63-
return oauth
63+
return oauth, access_token, refresh_token
6464

6565

6666
if __name__ == '__main__':

demo/cooperative_auth.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# coding: utf-8
2+
3+
from __future__ import unicode_literals, absolute_import
4+
5+
from logging import Logger
6+
from multiprocessing import Manager, Process
7+
from os import getpid
8+
9+
from boxsdk.auth.cooperatively_managed_oauth2 import CooperativelyManagedOAuth2
10+
from boxsdk.network.logging_network import LoggingNetwork
11+
from boxsdk.util.log import setup_logging
12+
from boxsdk import Client
13+
14+
from auth import authenticate, CLIENT_ID, CLIENT_SECRET
15+
16+
17+
def main():
18+
# Create a multiprocessing manager to use as the token store
19+
global tokens, refresh_lock
20+
manager = Manager()
21+
tokens = manager.Namespace()
22+
refresh_lock = manager.Lock()
23+
24+
# Authenticate in master process
25+
oauth2, tokens.access, tokens.refresh = authenticate(CooperativelyManagedOAuth2)
26+
27+
# Create 2 worker processes and wait on them to finish
28+
workers = []
29+
for _ in range(2):
30+
worker_process = Process(target=worker)
31+
worker_process.start()
32+
workers.append(worker_process)
33+
for worker_process in workers:
34+
worker_process.join()
35+
36+
37+
def _retrive_tokens():
38+
return tokens.access, tokens.refresh
39+
40+
41+
def _store_tokens(access_token, refresh_token):
42+
tokens.access, tokens.refresh = access_token, refresh_token
43+
44+
45+
def worker():
46+
# Set up a logging network, but use the LoggingProxy so we can see which PID is generating messages
47+
logger = setup_logging(name='boxsdk.network.{0}'.format(getpid()))
48+
logger_proxy = LoggerProxy(logger)
49+
logging_network = LoggingNetwork(logger)
50+
51+
# Create a coop oauth2 instance.
52+
# Tokens will be retrieved from and stored to the multiprocessing Namespace.
53+
# A multiprocessing Lock will be used to synchronize token refresh.
54+
# The tokens from the master process are used for initial auth.
55+
# Whichever process needs to refresh
56+
oauth2 = CooperativelyManagedOAuth2(
57+
retrieve_tokens=_retrive_tokens,
58+
client_id=CLIENT_ID,
59+
client_secret=CLIENT_SECRET,
60+
store_tokens=_store_tokens,
61+
network_layer=logging_network,
62+
access_token=tokens.access,
63+
refresh_token=tokens.refresh,
64+
refresh_lock=refresh_lock,
65+
)
66+
client = Client(oauth2, network_layer=logging_network)
67+
_do_work(client)
68+
69+
70+
def _do_work(client):
71+
# Do some work in a worker process.
72+
# To see token refresh, perhaps put this in a loop (and don't forget to sleep for a bit between requests).
73+
me = client.user(user_id='me').get()
74+
items = client.folder('0').get_items(10)
75+
76+
77+
class LoggerProxy(Logger):
78+
"""
79+
Proxy for a logger that injects the current PID before log messages.
80+
"""
81+
def __init__(self, logger):
82+
self._logger_log = logger._log
83+
logger._log = self._log
84+
self._preamble = 'PID {0}: '.format(getpid())
85+
86+
def _log(self, level, msg, args, exc_info=None, extra=None):
87+
msg = self._preamble + msg
88+
return self._logger_log(level, msg, args, exc_info=exc_info, extra=extra)
89+
90+
91+
if __name__ == '__main__':
92+
main()

demo/example.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def run_examples(oauth):
290290
def main():
291291

292292
# Please notice that you need to put in your client id and client secret in demo/auth.py in order to make this work.
293-
oauth = authenticate()
293+
oauth, _, _ = authenticate()
294294
run_examples(oauth)
295295
os._exit(0)
296296

demo/music_player.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __init__(self, folder_path):
1616
shuffle(self._mp3_files)
1717

1818
def _get_client(self):
19-
oauth = self._authenticate()
19+
oauth, _, _ = self._authenticate()
2020
return Client(oauth)
2121

2222
def _authenticate(self):

docs/source/boxsdk.auth.rst

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ boxsdk.auth package
44
Submodules
55
----------
66

7+
boxsdk.auth.cooperatively_managed_oauth2 module
8+
-----------------------------------------------
9+
10+
.. automodule:: boxsdk.auth.cooperatively_managed_oauth2
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
715
boxsdk.auth.jwt_auth module
816
---------------------------
917

@@ -28,10 +36,10 @@ boxsdk.auth.redis_managed_oauth2 module
2836
:undoc-members:
2937
:show-inheritance:
3038

31-
boxsdk.auth.remote_oauth2 module
32-
--------------------------------
39+
boxsdk.auth.remote_managed_oauth2 module
40+
----------------------------------------
3341

34-
.. automodule:: boxsdk.auth.remote_oauth2
42+
.. automodule:: boxsdk.auth.remote_managed_oauth2
3543
:members:
3644
:undoc-members:
3745
:show-inheritance:

test/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def retry_after_response(request):
7575
def server_error_response(request):
7676
mock_network_response = Mock(DefaultNetworkResponse)
7777
mock_network_response.status_code = int(request.param)
78+
mock_network_response.ok = False
7879
return mock_network_response
7980

8081

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# coding: utf-8
2+
3+
from __future__ import unicode_literals, absolute_import
4+
5+
from mock import Mock
6+
7+
from boxsdk.auth import cooperatively_managed_oauth2
8+
9+
10+
def test_cooperatively_managed_oauth2_calls_retrieve_tokens_during_refresh(access_token, refresh_token):
11+
retrieve_tokens = Mock()
12+
oauth2 = cooperatively_managed_oauth2.CooperativelyManagedOAuth2(
13+
retrieve_tokens=retrieve_tokens,
14+
client_id=None,
15+
client_secret=None,
16+
)
17+
retrieve_tokens.return_value = access_token, refresh_token
18+
assert oauth2.refresh(None) == (access_token, refresh_token)
19+
retrieve_tokens.assert_called_once_with()

test/unit/auth/test_jwt_auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ def jwt_auth_init_mocks(
7979
)
8080

8181
jwt_auth_open.assert_called_once_with(sentinel.rsa_path)
82-
key_file.return_value.read.assert_called_once_with()
82+
key_file.return_value.read.assert_called_once_with() # pylint:disable=no-member
8383
load_pem_private_key.assert_called_once_with(
84-
key_file.return_value.read.return_value,
84+
key_file.return_value.read.return_value, # pylint:disable=no-member
8585
password=rsa_passphrase,
8686
backend=default_backend(),
8787
)

0 commit comments

Comments
 (0)