5 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/66cda9143dc4/ Changeset: 66cda9143dc4 User: jmchilton Date: 2013-11-06 07:15:59 Summary: Implement mechanism allowing admin API actions without pre-existing user in database. This key can be set by configuring the new master_api_key option in universe_wsgi.ini. Only set this if you have good reason (e.g. boostrapping galaxy programatically). Affected #: 5 files diff -r e290746008f2951df07655df8086015fa3e0cf3f -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -316,6 +316,7 @@ self.biostar_key_name = kwargs.get( 'biostar_key_name', None ) self.biostar_key = kwargs.get( 'biostar_key', None ) self.pretty_datetime_format = expand_pretty_datetime_format( kwargs.get( 'pretty_datetime_format', '$locale (UTC)' ) ) + self.master_api_key = kwargs.get( 'master_api_key', None ) # Experimental: This will not be enabled by default and will hide # nonproduction code. # The api_folders refers to whether the API exposes the /folders section. diff -r e290746008f2951df07655df8086015fa3e0cf3f -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 lib/galaxy/security/passwords.py --- a/lib/galaxy/security/passwords.py +++ b/lib/galaxy/security/passwords.py @@ -2,9 +2,10 @@ import hashlib from struct import Struct from operator import xor -from itertools import izip, starmap +from itertools import starmap from os import urandom from base64 import b64encode +from galaxy.util import safe_str_cmp SALT_LENGTH = 12 KEY_LENGTH = 24 @@ -73,11 +74,3 @@ rv = starmap( xor, zip( rv, u ) ) #Python < 2.6.8: starmap requires function inputs to be tuples, so we need to use zip instead of izip buf.extend(rv) return ''.join(map(chr, buf))[:keylen] - -def safe_str_cmp(a, b): - if len(a) != len(b): - return False - rv = 0 - for x, y in izip(a, b): - rv |= ord(x) ^ ord(y) - return rv == 0 \ No newline at end of file diff -r e290746008f2951df07655df8086015fa3e0cf3f -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 lib/galaxy/util/__init__.py --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -7,6 +7,7 @@ from os.path import relpath from hashlib import md5 +from itertools import izip from galaxy import eggs @@ -930,6 +931,15 @@ else: return shutil.move( source, target ) + +def safe_str_cmp(a, b): + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + galaxy_root_path = os.path.join(__path__[0], "..","..","..") # The dbnames list is used in edit attributes and the upload tool diff -r e290746008f2951df07655df8086015fa3e0cf3f -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 lib/galaxy/web/framework/__init__.py --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -9,6 +9,7 @@ import socket import string import time +import hashlib from Cookie import CookieError @@ -21,6 +22,7 @@ from galaxy.util.json import to_json_string, from_json_string from galaxy.util.backports.importlib import import_module from galaxy.util.sanitize_html import sanitize_html +from galaxy.util import safe_str_cmp pkg_resources.require( "simplejson" ) import simplejson @@ -136,7 +138,7 @@ error_status = '403 Forbidden' if trans.error_message: return trans.error_message - if user_required and trans.user is None: + if user_required and trans.anonymous: error_message = "API Authentication Required for this request" return error if trans.request.body: @@ -332,7 +334,8 @@ Encapsulates web transaction specific state for the Galaxy application (specifically the user's "cookie" session and history) """ - def __init__( self, environ, app, webapp, session_cookie = None): + + def __init__( self, environ, app, webapp, session_cookie=None): self.app = app self.webapp = webapp self.security = webapp.security @@ -349,8 +352,7 @@ self.__user = None self.galaxy_session = None self.error_message = None - - + if self.environ.get('is_api_request', False): # With API requests, if there's a key, use it and associate the # user with the transaction. @@ -389,6 +391,10 @@ self.template_context.update ( dict( _=t.ugettext, n_=t.ugettext, N_=t.ungettext ) ) @property + def anonymous( self ): + return self.user is None and not self.api_inherit_admin + + @property def sa_session( self ): """ Returns a SQLAlchemy session -- currently just gets the current @@ -498,7 +504,13 @@ """ api_key = self.request.params.get('key', None) secure_id = self.get_cookie( name=session_cookie ) - if self.environ.get('is_api_request', False) and api_key: + sessionless_api_request = self.environ.get('is_api_request', False) and api_key + if sessionless_api_request and self._check_master_api_key( api_key ): + self.api_inherit_admin = True + log.info( "Session authenticated using Galaxy master api key" ) + self.user = None + self.galaxy_session = None + elif sessionless_api_request: # Sessionless API transaction, we just need to associate a user. try: provided_key = self.sa_session.query( self.app.model.APIKeys ).filter( self.app.model.APIKeys.table.c.key == api_key ).one() @@ -519,6 +531,15 @@ self.user = None self.galaxy_session = None + def _check_master_api_key( self, api_key ): + master_api_key = getattr( self.app.config, 'master_api_key', None ) + if not master_api_key: + return False + # Hash keys to make them the same size, so we can do safe comparison. + master_hash = hashlib.sha256( master_api_key ).hexdigest() + provided_hash = hashlib.sha256( api_key ).hexdigest() + return safe_str_cmp( master_hash, provided_hash ) + def _ensure_valid_session( self, session_cookie, create=True): """ Ensure that a valid Galaxy session exists and is available as diff -r e290746008f2951df07655df8086015fa3e0cf3f -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 universe_wsgi.ini.sample --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -635,6 +635,11 @@ # other users #api_allow_run_as = None +# Master key that allows many API admin actions to be used without actually +# having a defined admin user in the database/config. Only set this if you need +# to bootstrap Galaxy, you probably do not want to set this on public servers. +#master_api_key=seriouslychangethis + # Enable tool tags (associating tools with tags). This has its own option # since its implementation has a few performance implications on startup for # large servers. https://bitbucket.org/galaxy/galaxy-central/commits/ce74b826309d/ Changeset: ce74b826309d User: jmchilton Date: 2013-11-06 07:15:59 Summary: Allow user creation via API w/Galaxy auth. Previously user creation only worked when used with external authentication. Client code for testing available in blend4j: https://github.com/jmchilton/blend4j/commit/c3bc8cb782e8678956ce66563fc316a1.... Affected #: 3 files diff -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 -r ce74b826309d68cb9a48cc1ba393dea39d12f30e lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -252,6 +252,33 @@ # -- Mixins for working with Galaxy objects. -- # + +class CreatesUsersMixin: + """ + Mixin centralizing logic for user creation between web and API controller. + + Web controller handles additional features such e-mail subscription, activation, + user forms, etc.... API created users are much more vanilla for the time being. + """ + + def create_user( self, trans, email, username, password ): + user = trans.app.model.User( email=email ) + user.set_password_cleartext( password ) + user.username = username + if trans.app.config.user_activation_on: + user.active = False + else: + user.active = True # Activation is off, every new user is active by default. + trans.sa_session.add( user ) + trans.sa_session.flush() + trans.app.security_agent.create_private_user_role( user ) + if trans.webapp.name == 'galaxy': + # We set default user permissions, before we log in and set the default history permissions + trans.app.security_agent.user_set_default_permissions( user, + default_access_private=trans.app.config.new_user_dataset_access_role_default_private ) + return user + + class SharableItemSecurityMixin: """ Mixin for handling security for sharable items. """ diff -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 -r ce74b826309d68cb9a48cc1ba393dea39d12f30e lib/galaxy/webapps/galaxy/api/users.py --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -5,11 +5,13 @@ from paste.httpexceptions import HTTPBadRequest, HTTPNotImplemented from galaxy import util, web from galaxy.web.base.controller import BaseAPIController, UsesTagsMixin +from galaxy.web.base.controller import CreatesUsersMixin log = logging.getLogger( __name__ ) -class UserAPIController( BaseAPIController, UsesTagsMixin ): +class UserAPIController( BaseAPIController, UsesTagsMixin, CreatesUsersMixin ): + @web.expose_api def index( self, trans, deleted='False', **kwd ): @@ -89,10 +91,15 @@ raise HTTPNotImplemented( detail='User creation is not allowed in this Galaxy instance' ) if trans.app.config.use_remote_user and trans.user_is_admin(): user = trans.get_or_create_remote_user(remote_user_email=payload['remote_user_email']) - item = user.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id, - 'total_disk_usage': float } ) + elif trans.user_is_admin(): + username = payload[ 'username' ] + email = payload[ 'email' ] + password = payload[ 'password' ] + user = self.create_user( trans=trans, email=email, username=username, password=password ) else: raise HTTPNotImplemented() + item = user.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id, + 'total_disk_usage': float } ) return item @web.expose_api diff -r 66cda9143dc4c5d396c9d5cd77b2de403521cf20 -r ce74b826309d68cb9a48cc1ba393dea39d12f30e lib/galaxy/webapps/galaxy/controllers/user.py --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -24,6 +24,7 @@ from galaxy.web import url_for from galaxy.web.base.controller import BaseUIController from galaxy.web.base.controller import UsesFormDefinitionsMixin +from galaxy.web.base.controller import CreatesUsersMixin from galaxy.web.form_builder import CheckboxField from galaxy.web.form_builder import build_select_field from galaxy.web.framework.helpers import time_ago, grids @@ -56,7 +57,8 @@ def build_initial_query( self, trans, **kwd ): return trans.sa_session.query( self.model_class ).filter( self.model_class.user_id == trans.user.id ) -class User( BaseUIController, UsesFormDefinitionsMixin ): + +class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin ): user_openid_grid = UserOpenIDGrid() installed_len_files = None @@ -703,22 +705,10 @@ message = kwd.get( 'message', '' ) status = kwd.get( 'status', 'done' ) is_admin = cntrller == 'admin' and trans.user_is_admin() - user = trans.app.model.User( email=email ) - user.set_password_cleartext( password ) - user.username = username - if trans.app.config.user_activation_on: - user.active = False - else: - user.active = True # Activation is off, every new user is active by default. - trans.sa_session.add( user ) - trans.sa_session.flush() - trans.app.security_agent.create_private_user_role( user ) + user = self.create_user( trans=trans, email=email, username=username, password=password ) error = '' success = True if trans.webapp.name == 'galaxy': - # We set default user permissions, before we log in and set the default history permissions - trans.app.security_agent.user_set_default_permissions( user, - default_access_private=trans.app.config.new_user_dataset_access_role_default_private ) # Save other information associated with the user, if any user_info_forms = self.get_all_forms( trans, filter=dict( deleted=False ), https://bitbucket.org/galaxy/galaxy-central/commits/e3826e079fe1/ Changeset: e3826e079fe1 User: jmchilton Date: 2013-11-06 07:15:59 Summary: Add API method allowing admins to generate API methods for users. Moved some now common functionality between user web controller and user API controller into a new CreatesApiKeys mixin. Client code for testing available in blend4j: https://github.com/jmchilton/blend4j/commit/c3bc8cb782e8678956ce66563fc316a1.... Affected #: 4 files diff -r ce74b826309d68cb9a48cc1ba393dea39d12f30e -r e3826e079fe14781db695467c137f0cc509d6dc6 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -279,6 +279,21 @@ return user +class CreatesApiKeysMixin: + """ + Mixing centralizing logic for creating API keys for user objects. + """ + + def create_api_key( self, trans, user ): + guid = trans.app.security.get_new_guid() + new_key = trans.app.model.APIKeys() + new_key.user_id = user.id + new_key.key = guid + trans.sa_session.add( new_key ) + trans.sa_session.flush() + return guid + + class SharableItemSecurityMixin: """ Mixin for handling security for sharable items. """ diff -r ce74b826309d68cb9a48cc1ba393dea39d12f30e -r e3826e079fe14781db695467c137f0cc509d6dc6 lib/galaxy/webapps/galaxy/api/users.py --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -6,12 +6,12 @@ from galaxy import util, web from galaxy.web.base.controller import BaseAPIController, UsesTagsMixin from galaxy.web.base.controller import CreatesUsersMixin +from galaxy.web.base.controller import CreatesApiKeysMixin log = logging.getLogger( __name__ ) -class UserAPIController( BaseAPIController, UsesTagsMixin, CreatesUsersMixin ): - +class UserAPIController( BaseAPIController, UsesTagsMixin, CreatesUsersMixin, CreatesApiKeysMixin ): @web.expose_api def index( self, trans, deleted='False', **kwd ): @@ -103,6 +103,17 @@ return item @web.expose_api + @web.require_admin + def api_key( self, trans, user_id, **kwd ): + """ + POST /api/users/{encoded_user_id}/api_key + Creates a new API key for specified user. + """ + user = self.get_user( trans, user_id ) + key = self.create_api_key( trans, user ) + return key + + @web.expose_api def update( self, trans, **kwd ): raise HTTPNotImplemented() diff -r ce74b826309d68cb9a48cc1ba393dea39d12f30e -r e3826e079fe14781db695467c137f0cc509d6dc6 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -171,6 +171,9 @@ webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) + webapp.mapper.connect( "create_api_key", "/api/users/:user_id/api_key", + controller="users", action="api_key", user_id=None, conditions=dict( method=["POST"] ) ) + # visualizations registry generic template renderer webapp.add_route( '/visualization/show/:visualization_name', controller='visualization', action='render', visualization_name=None ) diff -r ce74b826309d68cb9a48cc1ba393dea39d12f30e -r e3826e079fe14781db695467c137f0cc509d6dc6 lib/galaxy/webapps/galaxy/controllers/user.py --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -25,6 +25,7 @@ from galaxy.web.base.controller import BaseUIController from galaxy.web.base.controller import UsesFormDefinitionsMixin from galaxy.web.base.controller import CreatesUsersMixin +from galaxy.web.base.controller import CreatesApiKeysMixin from galaxy.web.form_builder import CheckboxField from galaxy.web.form_builder import build_select_field from galaxy.web.framework.helpers import time_ago, grids @@ -58,7 +59,7 @@ return trans.sa_session.query( self.model_class ).filter( self.model_class.user_id == trans.user.id ) -class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin ): +class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, CreatesApiKeysMixin ): user_openid_grid = UserOpenIDGrid() installed_len_files = None @@ -1685,11 +1686,7 @@ message = util.restore_text( params.get( 'message', '' ) ) status = params.get( 'status', 'done' ) if params.get( 'new_api_key_button', False ): - new_key = trans.app.model.APIKeys() - new_key.user_id = trans.user.id - new_key.key = trans.app.security.get_new_guid() - trans.sa_session.add( new_key ) - trans.sa_session.flush() + self.create_api_key( trans, trans.user ) message = "Generated a new web API key" status = "done" return trans.fill_template( 'webapps/galaxy/user/api_keys.mako', https://bitbucket.org/galaxy/galaxy-central/commits/c7e80c863704/ Changeset: c7e80c863704 User: jmchilton Date: 2013-11-07 07:45:09 Summary: Do not allow deployer to use supplied example master_api_key. Really good point by @martenson. https://bitbucket.org/galaxy/galaxy-central/pull-request/251/api-enhancement... Affected #: 2 files diff -r e3826e079fe14781db695467c137f0cc509d6dc6 -r c7e80c863704463fa71062bb8f0745aaf667c8b9 lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -317,6 +317,9 @@ self.biostar_key = kwargs.get( 'biostar_key', None ) self.pretty_datetime_format = expand_pretty_datetime_format( kwargs.get( 'pretty_datetime_format', '$locale (UTC)' ) ) self.master_api_key = kwargs.get( 'master_api_key', None ) + if self.master_api_key == "changethis": # default in sample config file + raise Exception("Insecure configuration, please change master_api_key to something other than default (changethis)") + # Experimental: This will not be enabled by default and will hide # nonproduction code. # The api_folders refers to whether the API exposes the /folders section. diff -r e3826e079fe14781db695467c137f0cc509d6dc6 -r c7e80c863704463fa71062bb8f0745aaf667c8b9 universe_wsgi.ini.sample --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -638,7 +638,7 @@ # Master key that allows many API admin actions to be used without actually # having a defined admin user in the database/config. Only set this if you need # to bootstrap Galaxy, you probably do not want to set this on public servers. -#master_api_key=seriouslychangethis +#master_api_key=changethis # Enable tool tags (associating tools with tags). This has its own option # since its implementation has a few performance implications on startup for https://bitbucket.org/galaxy/galaxy-central/commits/8213c75972b3/ Changeset: 8213c75972b3 User: jmchilton Date: 2013-11-07 07:49:58 Summary: Fix(?) poorly worded variable name (per @natefoo's suggestion). Question mark added because I am not really sure I understand what this check (self.environ.get('is_api_request', False)) is all about, but like Nate said it doesn't matter functionally :). Affected #: 1 file diff -r c7e80c863704463fa71062bb8f0745aaf667c8b9 -r 8213c75972b3dd478c6448af9ce266fd57c82cf4 lib/galaxy/web/framework/__init__.py --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -504,13 +504,13 @@ """ api_key = self.request.params.get('key', None) secure_id = self.get_cookie( name=session_cookie ) - sessionless_api_request = self.environ.get('is_api_request', False) and api_key - if sessionless_api_request and self._check_master_api_key( api_key ): + api_key_supplied = self.environ.get('is_api_request', False) and api_key + if api_key_supplied and self._check_master_api_key( api_key ): self.api_inherit_admin = True log.info( "Session authenticated using Galaxy master api key" ) self.user = None self.galaxy_session = None - elif sessionless_api_request: + elif api_key_supplied: # Sessionless API transaction, we just need to associate a user. try: provided_key = self.sa_session.query( self.app.model.APIKeys ).filter( self.app.model.APIKeys.table.c.key == api_key ).one() Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.