diff --git a/qiita_db/handlers/artifact.py b/qiita_db/handlers/artifact.py index 82732863f..f33e0bc5d 100644 --- a/qiita_db/handlers/artifact.py +++ b/qiita_db/handlers/artifact.py @@ -11,7 +11,7 @@ from json import loads import qiita_db as qdb -from .oauth2 import OauthBaseHandler, authenticate_oauth +from .oauth2 import OauthBaseHandler, authenticate_oauth2 def _get_artifact(a_id): @@ -46,7 +46,7 @@ def _get_artifact(a_id): class ArtifactHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, artifact_id): """Retrieves the artifact information @@ -109,7 +109,7 @@ def get(self, artifact_id): self.write(response) - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def patch(self, artifact_id): """Patches the artifact information @@ -140,7 +140,7 @@ def patch(self, artifact_id): class ArtifactAPItestHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): """Creates a new artifact @@ -180,7 +180,7 @@ def post(self): class ArtifactTypeHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): """Creates a new artifact type diff --git a/qiita_db/handlers/core.py b/qiita_db/handlers/core.py index 9d3e380f1..b0cf38345 100644 --- a/qiita_db/handlers/core.py +++ b/qiita_db/handlers/core.py @@ -6,11 +6,11 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- -from .oauth2 import OauthBaseHandler, authenticate_oauth +from .oauth2 import OauthBaseHandler, authenticate_oauth2 import qiita_db as qdb class ResetAPItestHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): qdb.environment_manager.drop_and_rebuild_tst_database() diff --git a/qiita_db/handlers/oauth2.py b/qiita_db/handlers/oauth2.py index d156b5696..595108a0e 100644 --- a/qiita_db/handlers/oauth2.py +++ b/qiita_db/handlers/oauth2.py @@ -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: + 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): diff --git a/qiita_db/handlers/plugin.py b/qiita_db/handlers/plugin.py index 5850df51c..9f36cadce 100644 --- a/qiita_db/handlers/plugin.py +++ b/qiita_db/handlers/plugin.py @@ -12,7 +12,7 @@ from tornado.web import HTTPError -from .oauth2 import OauthBaseHandler, authenticate_oauth +from .oauth2 import OauthBaseHandler, authenticate_oauth2 from qiita_core.qiita_settings import qiita_config import qiita_db as qdb @@ -50,7 +50,7 @@ def _get_plugin(name, version): class PluginHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, name, version): """Retrieve the plugin information @@ -91,7 +91,7 @@ def get(self, name, version): class CommandListHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, name, version): with qdb.sql_connection.TRN: plugin = _get_plugin(name, version) @@ -154,7 +154,7 @@ def _get_command(plugin_name, plugin_version, cmd_name): class CommandHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, plugin_name, plugin_version, cmd_name): """Retrieve the command information @@ -193,7 +193,7 @@ def get(self, plugin_name, plugin_version, cmd_name): class CommandActivateHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, plugin_name, plugin_version, cmd_name): """Activates the command @@ -214,7 +214,7 @@ def post(self, plugin_name, plugin_version, cmd_name): class ReloadPluginAPItestHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): """Reloads the plugins""" conf_files = glob(join(qiita_config.plugin_dir, "*.conf")) diff --git a/qiita_db/handlers/prep_template.py b/qiita_db/handlers/prep_template.py index 0192ca773..cfbcd334b 100644 --- a/qiita_db/handlers/prep_template.py +++ b/qiita_db/handlers/prep_template.py @@ -13,7 +13,7 @@ import pandas as pd import qiita_db as qdb -from .oauth2 import OauthBaseHandler, authenticate_oauth +from .oauth2 import OauthBaseHandler, authenticate_oauth2 def _get_prep_template(pid): @@ -48,7 +48,7 @@ def _get_prep_template(pid): class PrepTemplateDBHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, prep_id): """Retrieves the prep template information @@ -89,7 +89,7 @@ def get(self, prep_id): class PrepTemplateDataHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, prep_id): """Retrieves the prep contents @@ -111,7 +111,7 @@ def get(self, prep_id): class PrepTemplateAPItestHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): prep_info_dict = loads(self.get_argument('prep_info')) study = self.get_argument('study') diff --git a/qiita_db/handlers/processing_job.py b/qiita_db/handlers/processing_job.py index efd5a94bd..4d9962fbc 100644 --- a/qiita_db/handlers/processing_job.py +++ b/qiita_db/handlers/processing_job.py @@ -13,7 +13,7 @@ from qiita_core.qiita_settings import qiita_config import qiita_db as qdb -from .oauth2 import OauthBaseHandler, authenticate_oauth +from .oauth2 import OauthBaseHandler, authenticate_oauth2 def _get_job(job_id): @@ -71,7 +71,7 @@ def _job_completer(job_id, payload): class JobHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, job_id): """Get the job information @@ -103,7 +103,7 @@ def get(self, job_id): class HeartbeatHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, job_id): """Update the heartbeat timestamp of the job @@ -124,7 +124,7 @@ def post(self, job_id): class ActiveStepHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, job_id): """Changes the current exectuion step of the given job @@ -146,7 +146,7 @@ def post(self, job_id): class CompleteHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, job_id): """Updates the job to one of the completed statuses: 'success', 'error' @@ -171,7 +171,7 @@ def post(self, job_id): class ProcessingJobAPItestHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): user = self.get_argument('user', 'test@foo.bar') s_name, s_version, cmd_name = loads(self.get_argument('command')) diff --git a/qiita_db/handlers/reference.py b/qiita_db/handlers/reference.py index ccc0638a5..04d6fc877 100644 --- a/qiita_db/handlers/reference.py +++ b/qiita_db/handlers/reference.py @@ -8,7 +8,7 @@ from tornado.web import HTTPError -from .oauth2 import OauthBaseHandler, authenticate_oauth +from .oauth2 import OauthBaseHandler, authenticate_oauth2 import qiita_db as qdb @@ -42,7 +42,7 @@ def _get_reference(r_id): class ReferenceHandler(OauthBaseHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, reference_id): """Retrieves the filepath information of the given reference diff --git a/qiita_db/handlers/tests/oauthbase.py b/qiita_db/handlers/tests/oauthbase.py index 2fad62ec2..f4450bdbe 100644 --- a/qiita_db/handlers/tests/oauthbase.py +++ b/qiita_db/handlers/tests/oauthbase.py @@ -9,5 +9,6 @@ def setUp(self): self.header = {'Authorization': 'Bearer ' + self.token} r_client.hset(self.token, 'timestamp', '12/12/12 12:12:00') r_client.hset(self.token, 'grant_type', 'client') + r_client.hset(self.token, 'client_id', 'foo') r_client.expire(self.token, 20) super(OauthTestingBase, self).setUp() diff --git a/qiita_db/handlers/tests/test_oauth2.py b/qiita_db/handlers/tests/test_oauth2.py index 2c4bd8f3d..0ec6b2157 100644 --- a/qiita_db/handlers/tests/test_oauth2.py +++ b/qiita_db/handlers/tests/test_oauth2.py @@ -10,9 +10,63 @@ from moi import r_client +from qiita_db.user import User +from qiita_db.handlers.oauth2 import _check_oauth2_header, authenticate_oauth2 from qiita_pet.test.tornado_test_base import TestHandlerBase +def make_mock_handler(): + class mock_object: + def __init__(self): + self.status = None + self.body = None + + def set_status(self, thing): + self.status = thing + + def write(self, thing): + self.body = thing + + def finish(self): + pass + + handler = mock_object() + handler.request = mock_object() + handler.request.headers = {} + + return handler + + +def make_mock_decorated_handler(default_public, inject_user): + class mock_object: + def __init__(self): + self.status = None + self.body = None + + def set_status(self, thing): + self.status = thing + + def write(self, thing): + self.body = thing + + def finish(self): + pass + + @authenticate_oauth2(default_public=default_public, + inject_user=inject_user) + def get(self, item, item2=None): + self.body = {'x': item, 'y': item2} + + def get_current_user(self): + return "Default get_current_user method" + + handler = mock_object() + handler.request = mock_object() + handler.request.headers = {} + + return handler + + class OAuth2BaseHandlerTests(TestHandlerBase): def setUp(self): # Create client test authentication token @@ -76,6 +130,163 @@ def test_authenticate_header_bad_header_type(self): 'error_description': 'Oauth2 error: invalid access token'} self.assertEqual(loads(obs.body), exp) + def test_check_oauth2_header_bad_token(self): + obj = make_mock_handler() + obj.request.headers['Authorization'] = 'Bearer BADTOKEN' + exp = ('invalid_grant', 'Oauth2 error: token has timed out', None) + self.assertEqual(_check_oauth2_header(obj), exp) + + def test_check_oauth2_header_bad_header(self): + obj = make_mock_handler() + obj.request.headers['Authorofthestuff'] = 'foo' + exp = ('invalid_request', 'Oauth2 error: invalid access token', None) + self.assertEqual(_check_oauth2_header(obj), exp) + + def test_check_oauth2_header_bad_header_format(self): + obj = make_mock_handler() + obj.request.headers['Authorization'] = 'Bear ' + self.user_token + exp = ('invalid_grant', 'Oauth2 error: invalid access token', None) + self.assertEqual(_check_oauth2_header(obj), exp) + + def test_check_oauth2_header_bad_header_format_toomany_tokens(self): + obj = make_mock_handler() + obj.request.headers['Authorization'] = 'Bear er ' + self.user_token + exp = ('invalid_grant', 'Oauth2 error: invalid access token', None) + self.assertEqual(_check_oauth2_header(obj), exp) + + def test_check_oauth2_header_valid(self): + obj = make_mock_handler() + obj.request.headers['Authorization'] = 'Bearer ' + self.client_token + self.assertEqual(_check_oauth2_header(obj), (None, None, + 'test123123123')) + + def test_check_oauth2_rate_limiting(self): + # Check rate limiting works + obj = make_mock_handler() + obj.request.headers['Authorization'] = 'Bearer ' + self.user_token + self.assertEqual(_check_oauth2_header(obj), (None, None, + 'testuser')) + + self.assertEqual(int(r_client.get(self.user_rate_key)), 1) + r_client.setex('testuser_test@foo.bar_daily_limit', 0, 2) + exp = ('invalid_grant', 'Oauth2 error: daily request limit reached', + None) + self.assertEqual(_check_oauth2_header(obj), exp) + + +class AuthorizeOauth2DecoratorTests(TestHandlerBase): + def setUp(self): + # Create client test authentication token + self.client_token = 'SOMEAUTHTESTINGTOKENHERE2122' + r_client.hset(self.client_token, 'timestamp', '12/12/12 12:12:00') + r_client.hset(self.client_token, 'client_id', + '19ndkO3oMKsoChjVVWluF7QkxHRfYhTKSFbAVt8IhK7gZgDaO4') + r_client.hset(self.client_token, 'grant_type', 'client') + r_client.expire(self.client_token, 5) + # Create username test authentication token + self.user_token = 'SOMEAUTHTESTINGTOKENHEREUSERNAME' + r_client.hset(self.user_token, 'timestamp', '12/12/12 12:12:00') + r_client.hset(self.user_token, 'client_id', + 'yKDgajoKn5xlOA8tpo48Rq8mWJkH9z4LBCx2SvqWYLIryaan2u') + r_client.hset(self.user_token, 'grant_type', 'password') + r_client.hset(self.user_token, 'user', 'test@foo.bar') + r_client.expire(self.user_token, 5) + # Create test access limit token + self.user_rate_key = 'testuser_test@foo.bar_daily_limit' + r_client.setex(self.user_rate_key, 2, 5) + super(AuthorizeOauth2DecoratorTests, self).setUp() + + def test_not_public_no_inject_user(self): + obj = make_mock_decorated_handler(False, False) + obj.request.headers['Authorization'] = 'Bearer ' + self.user_token + obj.get('item1', 'item2') + self.assertEqual(obj.body, {'x': 'item1', 'y': 'item2'}) + self.assertEqual(obj.get_current_user(), + "Default get_current_user method") + + def test_not_public_no_inject_user_bad_token(self): + obj = make_mock_decorated_handler(False, False) + token = 'Bearer ' + self.user_token + 'asdasd' + obj.request.headers['Authorization'] = token + obj.get('item1', 'item2') + exp = {'error': 'invalid_grant', + 'error_description': 'Oauth2 error: token has timed out'} + self.assertEqual(obj.status, 400) + self.assertEqual(obj.body, exp) + self.assertEqual(obj.get_current_user(), + "Default get_current_user method") + + def test_not_public_inject_user(self): + cid = 'yKDgajoKn5xlOA8tpo48Rq8mWJkH9z4LBCx2SvqWYLIryaan2u' + u = User('admin@foo.bar') + u.oauth_client_id = cid + + obj = make_mock_decorated_handler(False, True) + obj.request.headers['Authorization'] = 'Bearer ' + self.user_token + obj.get('item1', 'item2') + self.assertEqual(obj.body, {'x': 'item1', 'y': 'item2'}) + self.assertEqual(obj.get_current_user(), u) + + def test_not_public_inject_user_bad_auth(self): + obj = make_mock_decorated_handler(False, True) + token = 'Bearer ' + self.user_token + 'asdasd' + obj.request.headers['Authorization'] = token + obj.get('item1', 'item2') + exp = {'error': 'invalid_grant', + 'error_description': 'Oauth2 error: token has timed out'} + self.assertEqual(obj.status, 400) + self.assertEqual(obj.body, exp) + + # if we request a user injection, but we have a authentication error + # then we should _not_ actually inject the user + self.assertEqual(obj.get_current_user(), + "Default get_current_user method") + + def test_public_no_inject_user_notoken(self): + obj = make_mock_decorated_handler(True, False) + # no header info + obj.get('item1', 'item2') + self.assertEqual(obj.body, {'x': 'item1', 'y': 'item2'}) + self.assertEqual(obj.get_current_user(), + "Default get_current_user method") + + def test_public_no_inject_user_badtoken(self): + obj = make_mock_decorated_handler(True, False) + token = 'Bearer ' + self.user_token + 'asdasd' + obj.request.headers['Authorization'] = token + obj.get('item1', 'item2') + exp = {'error': 'invalid_grant', + 'error_description': 'Oauth2 error: token has timed out'} + self.assertEqual(obj.status, 400) + self.assertEqual(obj.body, exp) + self.assertEqual(obj.get_current_user(), + "Default get_current_user method") + + def test_public_no_inject_user_token(self): + obj = make_mock_decorated_handler(True, False) + obj.request.headers['Authorization'] = 'Bearer ' + self.user_token + obj.get('item1', 'item2') + self.assertEqual(obj.body, {'x': 'item1', 'y': 'item2'}) + self.assertEqual(obj.get_current_user(), + "Default get_current_user method") + + def test_public_inject_user_token(self): + cid = 'yKDgajoKn5xlOA8tpo48Rq8mWJkH9z4LBCx2SvqWYLIryaan2u' + u = User('admin@foo.bar') + u.oauth_client_id = cid + + obj = make_mock_decorated_handler(True, True) + obj.request.headers['Authorization'] = 'Bearer ' + self.user_token + obj.get('item1', 'item2') + self.assertEqual(obj.body, {'x': 'item1', 'y': 'item2'}) + self.assertEqual(obj.get_current_user(), u) + + def test_public_inject_user_notoken(self): + obj = make_mock_decorated_handler(True, True) + obj.get('item1', 'item2') + self.assertEqual(obj.body, {'x': 'item1', 'y': 'item2'}) + self.assertEqual(obj.get_current_user(), User('demo@microbio.me')) + class OAuth2HandlerTests(TestHandlerBase): def test_authenticate_client_header(self): diff --git a/qiita_db/support_files/patches/54.sql b/qiita_db/support_files/patches/54.sql new file mode 100644 index 000000000..4fc0ff798 --- /dev/null +++ b/qiita_db/support_files/patches/54.sql @@ -0,0 +1,7 @@ +-- Apr 3, 2017 +-- Linking qiita users to the oauth table +ALTER TABLE qiita.qiita_user ADD client_id varchar ; + +ALTER TABLE qiita.qiita_user ADD CONSTRAINT uc_qiita_user_client_id UNIQUE ( client_id ) ; + +ALTER TABLE qiita.qiita_user ADD CONSTRAINT fk_qiita_user_client_id FOREIGN KEY ( client_id ) REFERENCES qiita.oauth_identifiers( client_id ) ; diff --git a/qiita_db/support_files/qiita-db.dbs b/qiita_db/support_files/qiita-db.dbs index 1f7ebe2f5..fc00742ac 100644 --- a/qiita_db/support_files/qiita-db.dbs +++ b/qiita_db/support_files/qiita-db.dbs @@ -1389,15 +1389,22 @@ + + + + + + + @@ -1892,7 +1899,6 @@ Controlled Vocabulary]]> - @@ -1971,6 +1977,7 @@ Controlled Vocabulary]]> + analysis tables diff --git a/qiita_db/support_files/qiita-db.html b/qiita_db/support_files/qiita-db.html index 5c7f9311b..c92a99785 100644 --- a/qiita_db/support_files/qiita-db.html +++ b/qiita_db/support_files/qiita-db.html @@ -267,7 +267,7 @@ - + Group_users @@ -366,7 +366,7 @@ analysis_users references analysis ( analysis_id ) analysis_id + analysis_users references analysis ( analysis_id )' style='fill:#a1a0a0;'>analysis_id Foreign Key fk_analysis_users_user analysis_users references qiita_user ( email ) @@ -411,12 +411,7 @@ column_ontology references mixs_field_description ( column_name ) column_name - Foreign Key fk_user_user_level - qiita_user references user_level ( user_level_id ) - -user_level_id + column_ontology references mixs_field_description ( column_name )' style='fill:#a1a0a0;'>column_name Foreign Key fk_filepath filepath references filepath_type ( filepath_type_id ) @@ -456,7 +451,7 @@ collection_analysis references analysis ( analysis_id ) analysis_id + collection_analysis references analysis ( analysis_id )' style='fill:#a1a0a0;'>analysis_id Foreign Key fk_collection collection references qiita_user ( email ) @@ -471,7 +466,7 @@ collection_users references collection ( collection_id ) collection_id + collection_users references collection ( collection_id )' style='fill:#a1a0a0;'>collection_id Foreign Key fk_collection_user_email collection_users references qiita_user ( email ) @@ -506,7 +501,7 @@ message_user references message ( message_id ) message_id + message_user references message ( message_id )' style='fill:#a1a0a0;'>message_id Foreign Key fk_message_user_0 message_user references qiita_user ( email ) @@ -586,7 +581,7 @@ study_users references qiita_user ( email ) email + study_users references qiita_user ( email )' style='fill:#a1a0a0;'>email Foreign Key fk_study_user study references qiita_user ( email ) @@ -826,7 +821,7 @@ parent_processing_job references processing_job ( child_id -> processing_job_id ) child_id + parent_processing_job references processing_job ( child_id -> processing_job_id )' style='fill:#a1a0a0;'>child_id Foreign Key fk_processing_job_workflow processing_job_workflow references qiita_user ( email ) @@ -896,7 +891,7 @@ job references reference ( input_file_reference_id -> reference_id ) input_file_reference_id + job references reference ( input_file_reference_id -> reference_id )' style='fill:#a1a0a0;'>input_file_reference_id Foreign Key fk_processing_job_qiita_user processing_job references qiita_user ( email ) @@ -976,12 +971,22 @@ study_experimental_factor references study ( study_id ) study_id + study_experimental_factor references study ( study_id )' style='fill:#a1a0a0;'>study_id Foreign Key fk_study_tags_qiita_user study_tags references qiita_user ( email ) email + study_tags references qiita_user ( email )' style='fill:#a1a0a0;'>email + Foreign Key fk_user_user_level + qiita_user references user_level ( user_level_id ) + +user_level_id + Foreign Key fk_qiita_user_client_id + qiita_user references oauth_identifiers ( client_id ) + +client_id controlled_vocab_valuesTable qiita.controlled_vocab_values @@ -1173,39 +1178,6 @@ definitiondefinition text load_dateload_date date not null - - - -qiita_userTable qiita.qiita_user -Holds all user information - Primary Key ( email ) -emailemail varchar not null -Referred by analysis ( email ) -Referred by analysis_users ( email ) -Referred by collection ( email ) -Referred by collection_users ( email ) -Referred by message_user ( email ) -Referred by processing_job ( email ) -Referred by processing_job_workflow ( email ) -Referred by study ( email ) -Referred by study_users ( email ) -Referred by study_tags ( email ) - Index ( user_level_id ) -user_level_iduser_level_id integer not null default 5 -user level -References user_level ( user_level_id ) - passwordpassword varchar not null - namename varchar - affiliationaffiliation varchar - addressaddress varchar - phonephone varchar - user_verify_codeuser_verify_code varchar -Code for initial user email verification - pass_reset_codepass_reset_code varchar -Randomly generated code for password reset - pass_reset_timestamppass_reset_timestamp timestamp -Time the reset code was generated - @@ -1561,6 +1533,7 @@ study_idstudy_id bigserial not null Unique name for study Referred by investigation_study ( study_id ) +Referred by per_study_tags ( study_id ) Referred by sample_template_filepath ( study_id ) Referred by study_artifact ( study_id ) Referred by study_environmental_package ( study_id ) @@ -1569,8 +1542,7 @@ Referred by study_prep_template ( study_id ) Referred by study_publication ( study_id ) Referred by study_sample ( study_id ) -Referred by study_users ( study_id ) -Referred by per_study_tags ( study_id ) +Referred by study_users ( study_id ) Index ( email ) emailemail varchar not null Email of study owner @@ -1712,7 +1684,8 @@ oauth_identifiersTable qiita.oauth_identifiers Primary Key ( client_id ) client_idclient_id varchar(50) not null -Referred by oauth_software ( client_id ) +Referred by oauth_software ( client_id ) +Referred by qiita_user ( client_id ) client_secretclient_secret varchar(255) @@ -2348,6 +2321,42 @@ emailemail varchar not null References qiita_user ( email ) + + + +qiita_userTable qiita.qiita_user +Holds all user information + Primary Key ( email ) +emailemail varchar not null +Referred by analysis ( email ) +Referred by analysis_users ( email ) +Referred by collection ( email ) +Referred by collection_users ( email ) +Referred by message_user ( email ) +Referred by processing_job ( email ) +Referred by processing_job_workflow ( email ) +Referred by study ( email ) +Referred by study_tags ( email ) +Referred by study_users ( email ) + Index ( user_level_id ) +user_level_iduser_level_id integer not null default 5 +user level +References user_level ( user_level_id ) + passwordpassword varchar not null + namename varchar + affiliationaffiliation varchar + addressaddress varchar + phonephone varchar + user_verify_codeuser_verify_code varchar +Code for initial user email verification + pass_reset_codepass_reset_code varchar +Randomly generated code for password reset + pass_reset_timestamppass_reset_timestamp timestamp +Time the reset code was generated + Unique Index ( client_id ) +client_idclient_id varchar +References oauth_identifiers ( client_id ) +

@@ -3029,91 +3038,6 @@
-

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Table qiita_user
Holds all user information
*email varchar
*user_level_id integer DEFO 5 user level
*password varchar
 name varchar
 affiliation varchar
 address varchar
 phone varchar
 user_verify_code varchar Code for initial user email verification
 pass_reset_code varchar Randomly generated code for password reset
 pass_reset_timestamp timestamp Time the reset code was generated
Indexes
Pkpk_user ON email
 idx_user ON user_level_id
Foreign Keys
 fk_user_user_level ( user_level_id ) ref user_level (user_level_id)
-

@@ -7079,4 +7003,104 @@
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Table qiita_user
Holds all user information
*email varchar
*user_level_id integer DEFO 5 user level
*password varchar
 name varchar
 affiliation varchar
 address varchar
 phone varchar
 user_verify_code varchar Code for initial user email verification
 pass_reset_code varchar Randomly generated code for password reset
 pass_reset_timestamp timestamp Time the reset code was generated
 client_id varchar
Indexes
Pkpk_user ON email
 idx_user ON user_level_id
Uuc_qiita_user_client_id ON client_id
Foreign Keys
 fk_user_user_level ( user_level_id ) ref user_level (user_level_id)
 fk_qiita_user_client_id ( client_id ) ref oauth_identifiers (client_id)
+ \ No newline at end of file diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 0df6ccd11..7a9a2d407 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -71,7 +71,8 @@ def setUp(self): 'phone': '111-222-3344', 'pass_reset_code': None, 'pass_reset_timestamp': None, - 'user_verify_code': None + 'user_verify_code': None, + 'client_id': None } def tearDown(self): @@ -121,6 +122,7 @@ def test_create_user(self): 'user_verify_code': '', 'address': None, 'user_level_id': 5, + 'client_id': None, 'email': 'testcreateuser@test.bar'} self._check_correct_info(obs, exp) @@ -133,6 +135,29 @@ def test_create_user(self): [m_id]]) qdb.util.clear_system_messages() + def test_oauth_client_id_exists(self): + u = qdb.user.User('test@foo.bar') + client_id = '19ndkO3oMKsoChjVVWluF7QkxHRfYhTKSFbAVt8IhK7gZgDaO4' + u.oauth_client_id = client_id + self.assertEqual(u.oauth_client_id, client_id) + + def test_oauth_does_not_exist(self): + u = qdb.user.User('test@foo.bar') + with self.assertRaises(qdb.exceptions.QiitaDBUnknownIDError): + u.oauth_client_id = 'boaty mcboatface' + + def test_create_from_client_id(self): + u = qdb.user.User('test@foo.bar') + client_id = '19ndkO3oMKsoChjVVWluF7QkxHRfYhTKSFbAVt8IhK7gZgDaO4' + u.oauth_client_id = client_id + + new_u = qdb.user.User.from_client_id(client_id) + self.assertEqual(u.id, new_u.id) + + def test_create_from_client_id_does_not_exist(self): + with self.assertRaises(qdb.exceptions.QiitaDBUnknownIDError): + qdb.user.User.from_client_id('boaty mcboatface') + def test_create_user_info(self): user = qdb.user.User.create('testcreateuserinfo@test.bar', 'password', self.userinfo) @@ -153,6 +178,7 @@ def test_create_user_info(self): 'pass_reset_code': None, 'user_verify_code': '', 'user_level_id': 5, + 'client_id': None, 'email': 'testcreateuserinfo@test.bar'} self._check_correct_info(obs, exp) @@ -219,7 +245,8 @@ def test_get_info(self): 'pass_reset_code': None, 'pass_reset_timestamp': None, 'user_verify_code': None, - 'phone': '222-444-6789' + 'phone': '222-444-6789', + 'client_id': None } self.assertEqual(self.user.info, expinfo) diff --git a/qiita_db/test/test_util.py b/qiita_db/test/test_util.py index 177d69fe5..2dfb9f626 100644 --- a/qiita_db/test/test_util.py +++ b/qiita_db/test/test_util.py @@ -66,7 +66,7 @@ def test_get_table_cols(self): obs = qdb.util.get_table_cols("qiita_user") exp = {"email", "user_level_id", "password", "name", "affiliation", "address", "phone", "user_verify_code", "pass_reset_code", - "pass_reset_timestamp"} + "pass_reset_timestamp", "client_id"} self.assertEqual(set(obs), exp) def test_exists_table(self): diff --git a/qiita_db/user.py b/qiita_db/user.py index bd6a648d0..70c40a8de 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -91,6 +91,44 @@ def _check_id(self, id_): qdb.sql_connection.TRN.add(sql, [id_]) return qdb.sql_connection.TRN.execute_fetchlast() + @classmethod + def from_client_id(cls, client_id): + """Instantiate a User from a client_id + + Parameters + ---------- + client_id : str + The client_id for a qiita user. + + Returns + ------- + qdb.user.User + The user for the client_id + + Raises + ------ + qdb.exceptions.QiitaDBUnknownIDError + If the client identifier is not associated with any study. + """ + with qdb.sql_connection.TRN: + + sql = """SELECT EXISTS( + SELECT * FROM qiita.{0} WHERE client_id = %s)""" + sql = sql.format(cls._table) + qdb.sql_connection.TRN.add(sql, [client_id]) + + if not qdb.sql_connection.TRN.execute_fetchlast(): + raise qdb.exceptions.QiitaDBUnknownIDError(client_id, + cls._table) + + sql = """SELECT email + FROM qiita.{0} + WHERE client_id = %s""" + sql = sql.format(cls._table) + qdb.sql_connection.TRN.add(sql, [client_id]) + + return cls(qdb.sql_connection.TRN.execute_fetchlast()) + @classmethod def iter(cls): """Iterates over all users, sorted by their email addresses @@ -480,6 +518,34 @@ def unread_messages(self): qdb.sql_connection.TRN.add(sql, [self._id]) return qdb.sql_connection.TRN.execute_fetchindex() + @property + def oauth_client_id(self): + """Get an OAUTH2 client identifier for a user""" + with qdb.sql_connection.TRN: + sql = """SELECT client_id + FROM qiita.{0} + WHERE email = %s""".format(self._table) + qdb.sql_connection.TRN.add(sql, [self.id]) + return qdb.sql_connection.TRN.execute_fetchlast() + + @oauth_client_id.setter + def oauth_client_id(self, client_id): + """Set an OAUTH2 client identifier for a user""" + with qdb.sql_connection.TRN: + sql = """SELECT EXISTS( + SELECT * + FROM qiita.oauth_identifiers + WHERE client_id = %s)""" + qdb.sql_connection.TRN.add(sql, [client_id]) + if not qdb.sql_connection.TRN.execute_fetchlast(): + raise qdb.exceptions.QiitaDBUnknownIDError(client_id, + 'oauth_identifiers') + + sql = """UPDATE qiita.{0} + SET client_id = %s + WHERE email = %s""".format(self._table) + qdb.sql_connection.TRN.add(sql, [client_id, self.id]) + # ------- methods --------- def user_artifacts(self, artifact_type=None): """Returns the artifacts owned by the user, grouped by study diff --git a/qiita_pet/handlers/rest/study.py b/qiita_pet/handlers/rest/study.py index 25533ebe3..b2e9cc377 100644 --- a/qiita_pet/handlers/rest/study.py +++ b/qiita_pet/handlers/rest/study.py @@ -9,7 +9,7 @@ from tornado.escape import json_decode -from qiita_db.handlers.oauth2 import authenticate_oauth +from qiita_db.handlers.oauth2 import authenticate_oauth2 from qiita_db.study import StudyPerson, Study from qiita_db.user import User from .rest_handler import RESTHandler @@ -18,7 +18,7 @@ class StudyHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, study_id): study = self.safe_get_study(study_id) if study is None: @@ -44,7 +44,7 @@ def get(self, study_id): class StudyCreatorHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self): try: payload = json_decode(self.request.body) @@ -114,7 +114,7 @@ def post(self): class StudyStatusHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, study_id): study = self.safe_get_study(study_id) if study is None: diff --git a/qiita_pet/handlers/rest/study_person.py b/qiita_pet/handlers/rest/study_person.py index 160e68bfc..4217445f8 100644 --- a/qiita_pet/handlers/rest/study_person.py +++ b/qiita_pet/handlers/rest/study_person.py @@ -6,14 +6,14 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- -from qiita_db.handlers.oauth2 import authenticate_oauth +from qiita_db.handlers.oauth2 import authenticate_oauth2 from qiita_db.study import StudyPerson from qiita_db.exceptions import QiitaDBLookupError from .rest_handler import RESTHandler class StudyPersonHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, *args, **kwargs): name = self.get_argument('name') affiliation = self.get_argument('affiliation') @@ -28,7 +28,7 @@ def get(self, *args, **kwargs): 'id': p.id}) self.finish() - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, *args, **kwargs): name = self.get_argument('name') affiliation = self.get_argument('affiliation') diff --git a/qiita_pet/handlers/rest/study_preparation.py b/qiita_pet/handlers/rest/study_preparation.py index 02d9c5f1b..e108cf539 100644 --- a/qiita_pet/handlers/rest/study_preparation.py +++ b/qiita_pet/handlers/rest/study_preparation.py @@ -16,7 +16,7 @@ from qiita_pet.handlers.util import to_int from qiita_db.exceptions import QiitaDBUnknownIDError, QiitaError from qiita_db.metadata_template.prep_template import PrepTemplate -from qiita_db.handlers.oauth2 import authenticate_oauth +from qiita_db.handlers.oauth2 import authenticate_oauth2 from .rest_handler import RESTHandler @@ -24,8 +24,7 @@ class StudyPrepCreatorHandler(RESTHandler): # TODO: do something smart about warnings, perhaps this should go in its # own endpoint i.e. /api/v1/study//preparation/validate # See also: https://github.com/biocore/qiita/issues/2096 - - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, study_id, *args, **kwargs): data_type = self.get_argument('data_type') investigation_type = self.get_argument('investigation_type', None) @@ -51,7 +50,7 @@ def post(self, study_id, *args, **kwargs): class StudyPrepArtifactCreatorHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def post(self, study_id, prep_id): study = self.safe_get_study(study_id) if study is None: diff --git a/qiita_pet/handlers/rest/study_samples.py b/qiita_pet/handlers/rest/study_samples.py index a9f420dbe..0287d9eea 100644 --- a/qiita_pet/handlers/rest/study_samples.py +++ b/qiita_pet/handlers/rest/study_samples.py @@ -8,13 +8,13 @@ from tornado.escape import json_encode, json_decode import pandas as pd -from qiita_db.handlers.oauth2 import authenticate_oauth +from qiita_db.handlers.oauth2 import authenticate_oauth2 from .rest_handler import RESTHandler class StudySamplesHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, study_id): study = self.safe_get_study(study_id) if study is None: @@ -28,7 +28,7 @@ def get(self, study_id): self.write(json_encode(samples)) self.finish() - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def patch(self, study_id): study = self.safe_get_study(study_id) if study is None: @@ -83,7 +83,7 @@ def patch(self, study_id): class StudySamplesCategoriesHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, study_id, categories): if not categories: self.fail('No categories specified', 405) @@ -118,7 +118,7 @@ def get(self, study_id, categories): class StudySamplesInfoHandler(RESTHandler): - @authenticate_oauth + @authenticate_oauth2(default_public=False, inject_user=False) def get(self, study_id): study = self.safe_get_study(study_id) if study is None: