-
Notifications
You must be signed in to change notification settings - Fork 219
Add network and auth subclasses. #81
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e48c9e3
Add network and auth subclasses.
Jeff-Meadows e2135bb
Override _store_tokens instead of send_token_request in Redis Auth.
Jeff-Meadows 2bb05e3
Fix rst files and update docs.
Jeff-Meadows b35881f
Add tests for new auth and logging classes.
Jeff-Meadows 97a7774
Merge branch 'master' into auth
Jeff-Meadows 3fff540
Fix pylint errors.
Jeff-Meadows File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # coding: utf-8 | ||
|
|
||
| from __future__ import unicode_literals | ||
| from boxsdk import OAuth2 | ||
|
|
||
|
|
||
| class CooperativelyManagedOAuth2Mixin(OAuth2): | ||
| """ | ||
| Box SDK OAuth2 mixin. | ||
| Allows for sharing auth tokens between multiple clients. | ||
| """ | ||
| def __init__(self, retrieve_tokens=None, *args, **kwargs): | ||
| """ | ||
| :param retrieve_tokens: | ||
| Callback to get the current access/refresh token pair. | ||
| :type retrieve_tokens: | ||
| `callable` of () => (`unicode`, `unicode`) | ||
| """ | ||
| self._retrieve_tokens = retrieve_tokens | ||
| super(CooperativelyManagedOAuth2Mixin, self).__init__(*args, **kwargs) | ||
|
|
||
| def _get_tokens(self): | ||
| """ | ||
| Base class override. Get the tokens from the user-specified callback. | ||
| """ | ||
| return self._retrieve_tokens() | ||
|
|
||
|
|
||
| class CooperativelyManagedOAuth2(CooperativelyManagedOAuth2Mixin): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we create a concrete class for JWT as well? |
||
| """ | ||
| Box SDK OAuth2 subclass. | ||
| Allows for sharing auth tokens between multiple clients. The retrieve_tokens callback should | ||
| return the current access/refresh token pair. | ||
| """ | ||
| pass | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # coding: utf-8 | ||
|
|
||
| from __future__ import unicode_literals | ||
| from redis import StrictRedis | ||
| from redis.lock import Lock | ||
| from uuid import uuid4 | ||
| from boxsdk import JWTAuth, OAuth2 | ||
|
|
||
|
|
||
| class RedisManagedOAuth2Mixin(OAuth2): | ||
| """ | ||
| Box SDK OAuth2 subclass. | ||
| Allows for storing auth tokens in redis. | ||
|
|
||
| :param unique_id: | ||
| An identifier for this auth object. Auth instances which wish to share tokens must use the same ID. | ||
| :type unique_id: | ||
| `unicode` | ||
| :param redis_server: | ||
| An instance of a Redis server, configured to talk to Redis. | ||
| :type redis_server: | ||
| :class:`Redis` | ||
| """ | ||
| def __init__(self, unique_id=uuid4(), redis_server=None, *args, **kwargs): | ||
| self._unique_id = unique_id | ||
| self._redis_server = redis_server or StrictRedis() | ||
| refresh_lock = Lock(redis=self._redis_server, name='{0}_lock'.format(self._unique_id)) | ||
| super(RedisManagedOAuth2Mixin, self).__init__(*args, refresh_lock=refresh_lock, **kwargs) | ||
| if self._access_token is None: | ||
| self._update_current_tokens() | ||
|
|
||
| def _update_current_tokens(self): | ||
| """ | ||
| Get the latest tokens from redis and store them. | ||
| """ | ||
| self._access_token, self._refresh_token = self._redis_server.hvals(self._unique_id) or (None, None) | ||
|
|
||
| @property | ||
| def unique_id(self): | ||
| """ | ||
| Get the unique ID used by this auth instance. Other instances can share tokens with this instance | ||
| if they share the ID with this instance. | ||
| """ | ||
| return self._unique_id | ||
|
|
||
| def _get_tokens(self): | ||
| """ | ||
| Base class override. | ||
| Gets the latest tokens from redis before returning them. | ||
| """ | ||
| self._update_current_tokens() | ||
| return super(RedisManagedOAuth2Mixin, self)._get_tokens() | ||
|
|
||
| def _store_tokens(self, access_token, refresh_token): | ||
| """ | ||
| Base class override. | ||
| Saves the refreshed tokens in redis. | ||
| """ | ||
| super(RedisManagedOAuth2Mixin, self)._store_tokens(access_token, refresh_token) | ||
| self._redis_server.hmset(self._unique_id, {'access': access_token, 'refresh': refresh_token}) | ||
|
|
||
|
|
||
| class RedisManagedOAuth2(RedisManagedOAuth2Mixin): | ||
| """ | ||
| OAuth2 subclass which uses Redis to manage tokens. | ||
| """ | ||
| pass | ||
|
|
||
|
|
||
| class RedisManagedJWTAuth(RedisManagedOAuth2Mixin, JWTAuth): | ||
| """ | ||
| JWT Auth subclass which uses Redis to manage access tokens. | ||
| """ | ||
| def _auth_with_jwt(self, sub, sub_type): | ||
| """ | ||
| Base class override. Returns the access token in a tuple to match the OAuth2 interface. | ||
| """ | ||
| return super(RedisManagedJWTAuth, self)._auth_with_jwt(sub, sub_type), None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # coding: utf-8 | ||
|
|
||
| from __future__ import unicode_literals | ||
| from boxsdk import OAuth2 | ||
|
|
||
|
|
||
| class RemoteOAuth2Mixin(OAuth2): | ||
| """ | ||
| Box SDK OAuth2 mixin. | ||
| Allows for storing auth tokens remotely. | ||
|
|
||
| """ | ||
| def __init__(self, retrieve_access_token=None, *args, **kwargs): | ||
| """ | ||
| :param retrieve_access_token: | ||
| Callback to exchange an existing access token for a new one. | ||
| :type retrieve_access_token: | ||
| `callable` of `unicode` => `unicode` | ||
| """ | ||
| self._retrieve_access_token = retrieve_access_token | ||
| super(RemoteOAuth2Mixin, self).__init__(*args, **kwargs) | ||
|
|
||
| def _refresh(self, access_token): | ||
| """ | ||
| Base class override. Ask the remote host for a new token. | ||
| """ | ||
| self._access_token = self._retrieve_access_token(access_token) | ||
| return self._access_token, None | ||
|
|
||
|
|
||
| class RemoteOAuth2(RemoteOAuth2Mixin): | ||
| """ | ||
| Box SDK OAuth2 subclass. | ||
| Allows for storing auth tokens remotely. The retrieve_access_token callback should | ||
| return an access token, presumably acquired from a remote server on which your auth credentials are available. | ||
| """ | ||
| pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # coding: utf-8 | ||
|
|
||
| from __future__ import unicode_literals | ||
| from pprint import pformat | ||
| from boxsdk.network.default_network import DefaultNetwork | ||
| from boxsdk.util.log import setup_logging | ||
|
|
||
|
|
||
| class LoggingNetwork(DefaultNetwork): | ||
| """ | ||
| SDK Network subclass that logs requests and responses. | ||
| """ | ||
| LOGGER_NAME = 'boxsdk.network' | ||
| REQUEST_FORMAT = '\x1b[36m%s %s %s\x1b[0m' | ||
| SUCCESSFUL_RESPONSE_FORMAT = '\x1b[32m%s\x1b[0m' | ||
| ERROR_RESPONSE_FORMAT = '\x1b[31m%s\n%s\n%s\n\x1b[0m' | ||
|
|
||
| def __init__(self, logger=None): | ||
| """ | ||
| :param logger: | ||
| The logger to use. If you instantiate this class more than once, you should use the same logger | ||
| to avoid duplicate log entries. | ||
| :type logger: | ||
| :class:`Logger` | ||
| """ | ||
| super(LoggingNetwork, self).__init__() | ||
| self._logger = logger or setup_logging(name=self.LOGGER_NAME) | ||
|
|
||
| @property | ||
| def logger(self): | ||
| return self._logger | ||
|
|
||
| def _log_request(self, method, url, **kwargs): | ||
| """ | ||
| Logs information about the Box API request. | ||
|
|
||
| :param method: | ||
| The HTTP verb that should be used to make the request. | ||
| :type method: | ||
| `unicode` | ||
| :param url: | ||
| The URL for the request. | ||
| :type url: | ||
| `unicode` | ||
| :param access_token: | ||
| The OAuth2 access token used to authorize the request. | ||
| :type access_token: | ||
| `unicode` | ||
| """ | ||
| self._logger.info(self.REQUEST_FORMAT, method, url, pformat(kwargs)) | ||
|
|
||
| def _log_response(self, response): | ||
| """ | ||
| Logs information about the Box API response. | ||
|
|
||
| :param response: The Box API response. | ||
| """ | ||
| if response.ok: | ||
| self._logger.info(self.SUCCESSFUL_RESPONSE_FORMAT, response.content) | ||
| else: | ||
| self._logger.warning( | ||
| self.ERROR_RESPONSE_FORMAT, | ||
| response.status_code, | ||
| response.headers, | ||
| pformat(response.content), | ||
| ) | ||
|
|
||
| def request(self, method, url, access_token, **kwargs): | ||
| """ | ||
| Base class override. Logs information about an API request and response in addition to making the request. | ||
| """ | ||
| self._log_request(method, url, **kwargs) | ||
| response = super(LoggingNetwork, self).request(method, url, access_token, **kwargs) | ||
| self._log_response(response) | ||
| return response |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # coding: utf-8 | ||
|
|
||
| from __future__ import unicode_literals | ||
| import logging | ||
| import sys | ||
|
|
||
|
|
||
| def setup_logging(stream_or_file=None, debug=False, name=None): | ||
| """ | ||
| Create a logger for communicating with the user or writing to log files. | ||
| By default, creates a root logger that prints to stdout. | ||
|
|
||
| :param stream_or_file: | ||
| The destination of the log messages. If None, stdout will be used. | ||
| :type stream_or_file: | ||
| `unicode` or `file` or None | ||
| :param debug: | ||
| Whether or not the logger will be at the DEBUG level (if False, the logger will be at the INFO level). | ||
| :type debug: | ||
| `bool` or None | ||
| :param name: | ||
| The logging channel. If None, a root logger will be created. | ||
| :type name: | ||
| `unicode` or None | ||
| :return: | ||
| A logger that's been set up according to the specified parameters. | ||
| :rtype: | ||
| :class:`Logger` | ||
| """ | ||
| logger = logging.getLogger(name) | ||
| if isinstance(stream_or_file, basestring): | ||
| handler = logging.FileHandler(stream_or_file, mode='w') | ||
| else: | ||
| handler = logging.StreamHandler(stream_or_file or sys.stdout) | ||
| logger.addHandler(handler) | ||
| logger.setLevel(logging.DEBUG if debug else logging.INFO) | ||
| return logger |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The callable might not be able to retrieve anything (e.g., the cache might not have this user yet). Should we allow the callable to return None, or allow the tuple elements to be None?