-
Notifications
You must be signed in to change notification settings - Fork 80
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
OAuth2 support allowing public and user access #2099
Closed
Closed
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
8ce9538
API: Link oauth table to qiita user table
ElDeveloper fe78f95
API: Add unique constraint
ElDeveloper f67fffa
API/TST: Link User object with client_id
ElDeveloper e23a743
BUG: Fix problem with missing schema name
ElDeveloper bce6bd1
TST/MAINT: decompose oauth2 decorator; add explicit tests
wasade 8bae34d
Merge branch 'death-april' of github.com:eldeveloper/qiita into death…
wasade 9f94002
ENH: public defaulting oauth decorator
wasade 2afae75
MAINT/TST/ENH: refactor authorize_oauth
wasade bbbeadb
MAINT: rest handlers to use new oauth decorator
wasade bacde60
MAINT: remove old oauth decorator from use
wasade 058d122
MAINT: remove decorator instance
wasade f5f6a3c
Merge branch 'master' of github.com:biocore/qiita into death-april
wasade d697b81
DOC: revised docstring for authenticate_oauth2
wasade 693f3c9
STY: flake8
wasade 9c62dad
BUG: oauth test base wasn't specifying a client id
wasade c13c50e
TST: Fix some tests for user.py
ElDeveloper b839bfe
TST: Fix missing column name from tests
ElDeveloper 0b186a2
TST: addressing @antgonza's comment
wasade 15211de
Merge branch 'death-april' of github.com:wasade/qiita into death-april
wasade 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 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 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 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 |
---|---|---|
|
@@ -47,66 +47,138 @@ def _oauth_error(handler, error_msg, error): | |
handler.finish() | ||
|
||
|
||
def authenticate_oauth(f): | ||
def _check_oauth2_header(handler): | ||
"""Check if the oauth2 header is valid | ||
|
||
Parameters | ||
---------- | ||
handler : tornado.web.RequestHandler instance | ||
The handler instance being requested | ||
|
||
Returns | ||
------- | ||
errtype | ||
The type of error, None if no error was observed | ||
errdesc | ||
A description of the error, None if no error was observed. | ||
client_id | ||
The observed client ID. This field is None if any error was observed. | ||
""" | ||
header = handler.request.headers.get('Authorization', None) | ||
|
||
if header is None: | ||
return ('invalid_request', 'Oauth2 error: invalid access token', None) | ||
|
||
token_info = header.split() | ||
# Based on RFC6750 if reply is not 2 elements in the format of: | ||
# ['Bearer', token] we assume a wrong reply | ||
if len(token_info) != 2 or token_info[0] != 'Bearer': | ||
return ('invalid_grant', 'Oauth2 error: invalid access token', None) | ||
|
||
token = token_info[1] | ||
db_token = r_client.hgetall(token) | ||
if not db_token: | ||
# token has timed out or never existed | ||
return ('invalid_grant', 'Oauth2 error: token has timed out', None) | ||
|
||
# Check daily rate limit for key if password style key | ||
if db_token['grant_type'] == 'password': | ||
limit_key = '%s_%s_daily_limit' % (db_token['client_id'], | ||
db_token['user']) | ||
limiter = r_client.get(limit_key) | ||
if limiter is None: | ||
# Set limit to 5,000 requests per day | ||
r_client.setex(limit_key, 5000, 86400) | ||
else: | ||
r_client.decr(limit_key) | ||
if int(r_client.get(limit_key)) <= 0: | ||
return ('invalid_grant', | ||
'Oauth2 error: daily request limit reached', None) | ||
|
||
return (None, None, db_token['client_id']) | ||
|
||
|
||
class authenticate_oauth2: | ||
"""Decorate methods to require valid Oauth2 Authorization header[1] | ||
|
||
If a valid header is given, the handoff is done and the page is rendered. | ||
If an invalid header is given, a 400 error code is returned and the json | ||
error message is automatically sent. | ||
|
||
Returns | ||
------- | ||
Sends oauth2 formatted error JSON if authorizaton fails | ||
|
||
Notes | ||
----- | ||
Expects handler to be a tornado RequestHandler or subclass | ||
Attributes | ||
---------- | ||
default_public : bool | ||
If True, execute the handler if a) the oauth2 token is acceptable or | ||
b) if the Authorization header is not present. If False, the handler | ||
will only be executed if the oauth2 token is acceptable. | ||
inject_user : bool | ||
If True, monkey patch the handler's get_current_user method to return | ||
the instance of the User associated with the token's client ID. If | ||
False, get_current_user is not monkey patched. If default_public is | ||
also True, the default User returned is "demo@microbio.me" | ||
|
||
References | ||
---------- | ||
[1] The OAuth 2.0 Authorization Framework. | ||
http://tools.ietf.org/html/rfc6749 | ||
""" | ||
@functools.wraps(f) | ||
def wrapper(handler, *args, **kwargs): | ||
header = handler.request.headers.get('Authorization', None) | ||
if header is None: | ||
_oauth_error(handler, 'Oauth2 error: invalid access token', | ||
'invalid_request') | ||
return | ||
token_info = header.split() | ||
# Based on RFC6750 if reply is not 2 elements in the format of: | ||
# ['Bearer', token] we assume a wrong reply | ||
if len(token_info) != 2 or token_info[0] != 'Bearer': | ||
_oauth_error(handler, 'Oauth2 error: invalid access token', | ||
'invalid_grant') | ||
return | ||
def __init__(self, default_public=False, inject_user=False): | ||
self.default_public = default_public | ||
self.inject_user = inject_user | ||
|
||
def get_user_maker(self, cid): | ||
"""Produce a function which acts like get_current_user""" | ||
def f(): | ||
if cid is None: | ||
return qdb.user.User("demo@microbio.me") | ||
else: | ||
return qdb.user.User.from_client_id(cid) | ||
return f | ||
|
||
token = token_info[1] | ||
db_token = r_client.hgetall(token) | ||
if not db_token: | ||
# token has timed out or never existed | ||
_oauth_error(handler, 'Oauth2 error: token has timed out', | ||
'invalid_grant') | ||
return | ||
# Check daily rate limit for key if password style key | ||
if db_token['grant_type'] == 'password': | ||
limit_key = '%s_%s_daily_limit' % (db_token['client_id'], | ||
db_token['user']) | ||
limiter = r_client.get(limit_key) | ||
if limiter is None: | ||
# Set limit to 5,000 requests per day | ||
r_client.setex(limit_key, 5000, 86400) | ||
def __call__(self, f): | ||
"""Handle oauth, and execute the handler's method if appropriate | ||
|
||
Parameters | ||
---------- | ||
f : function | ||
The function decorated is expected to be a member method of a | ||
subclass of `Tornado.web.RequestHandler` | ||
|
||
Notes | ||
----- | ||
If an error with oauth2 occurs, a status code of 400 is set, a message | ||
about the error is sent out over `write` and the response is ended | ||
with `finish`. This happens without control being passed to the | ||
handler, and in this situation, the handler is not executed. | ||
""" | ||
@functools.wraps(f) | ||
def wrapper(handler, *args, **kwargs): | ||
errtype, errdesc, cid = _check_oauth2_header(handler) | ||
|
||
if self.default_public: | ||
# no error, or no authorization header. We should error if | ||
# oauth is actually attempted but there was an auth issue | ||
# (e.g., rate limit hit) | ||
if errtype not in (None, 'invalid_request'): | ||
_oauth_error(handler, errdesc, errtype) | ||
return | ||
|
||
if self.inject_user: | ||
handler.get_current_user = self.get_user_maker(cid) | ||
else: | ||
r_client.decr(limit_key) | ||
if int(r_client.get(limit_key)) <= 0: | ||
_oauth_error( | ||
handler, 'Oauth2 error: daily request limit reached', | ||
'invalid_grant') | ||
if errtype is not None: | ||
_oauth_error(handler, errdesc, errtype) | ||
return | ||
if self.inject_user: | ||
if cid is None: | ||
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. test? |
||
raise ValueError("cid is None, without an oauth " | ||
"error. This should never happen.") | ||
else: | ||
handler.get_current_user = self.get_user_maker(cid) | ||
|
||
return f(handler, *args, **kwargs) | ||
|
||
return f(handler, *args, **kwargs) | ||
return wrapper | ||
return wrapper | ||
|
||
|
||
class OauthBaseHandler(RequestHandler): | ||
|
This file contains 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 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 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
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.
test?