diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 24f93a83e5f..74370a8a6fa 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -14,6 +14,9 @@ import tempfile import galaxy.datatypes.registry from galaxy.datatypes.metadata import MetadataCollection +import logging +log = logging.getLogger( __name__ ) + datatypes_registry = galaxy.datatypes.registry.Registry() #Default Value Required for unit tests def set_datatypes_registry( d_registry ): @@ -121,9 +124,12 @@ class History( object ): last_hid = dataset.hid return last_hid + 1 - def add_galaxy_session( self, galaxy_session ): - self.galaxy_sessions.append( GalaxySessionToHistoryAssociation( galaxy_session, self ) ) - + def add_galaxy_session( self, galaxy_session, association=None ): + if association is None: + self.galaxy_sessions.append( GalaxySessionToHistoryAssociation( galaxy_session, self ) ) + else: + self.galaxy_sessions.append( association ) + def add_dataset( self, dataset, parent_id=None, genome_build=None, set_hid = True ): if parent_id: for data in self.datasets: @@ -487,16 +493,23 @@ class Event( object ): self.message = message class GalaxySession( object ): - def __init__( self, id=None, user=None, remote_host=None, remote_addr=None, referer=None ): + def __init__( self, id=None, user=None, remote_host=None, remote_addr=None, referer=None, current_history_id=None, session_key=None, is_valid=False, prev_session_id=None ): self.id = id self.user = user self.remote_host = remote_host self.remote_addr = remote_addr self.referer = referer + self.current_history_id = current_history_id + self.session_key = session_key + self.is_valid = is_valid + self.prev_session_id = prev_session_id self.histories = [] - def add_history( self, history ): - self.histories.append( GalaxySessionToHistoryAssociation( self, history ) ) + def add_history( self, history, association=None ): + if association is None: + self.histories.append( GalaxySessionToHistoryAssociation( self, history ) ) + else: + self.histories.append( association ) class GalaxySessionToHistoryAssociation( object ): def __init__( self, galaxy_session, history ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index d4f34231d84..4b2011aec51 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -173,7 +173,12 @@ GalaxySession.table = Table( "galaxy_session", metadata, Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=True ), Column( "remote_host", String( 255 ) ), Column( "remote_addr", String( 255 ) ), - Column( "referer", TEXT ) ) + Column( "referer", TEXT ), + Column( "current_history_id", Integer, ForeignKey( "history.id" ), nullable=True ), + Column( "session_key", TrimmedString( 255 ), index=True, unique=True ), # unique 128 bit random number coerced to a string + Column( "is_valid", Boolean, default=False ), + Column( "prev_session_id", Integer ) # saves a reference to the previous session so we have a way to chain them together + ) GalaxySessionToHistoryAssociation.table = Table( "galaxy_session_to_history", metadata, Column( "id", Integer, primary_key=True ), diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 119711130cf..7011e69ae26 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -411,7 +411,6 @@ class RootController( BaseController ): new_history = history.copy() new_history.name = history.name+" from "+user.email new_history.user_id = send_to_user.id - new_history.add_galaxy_session(trans.get_galaxy_session( create=True )) trans.log_event( "History share, id: %s, name: '%s': to new id: %s" % (str(history.id), history.name, str(new_history.id)) ) self.app.model.flush() return trans.show_message( "History (%s) has been shared with: %s" % (",".join(history_names),email) ) @@ -449,7 +448,12 @@ class RootController( BaseController ): new_history = import_history.copy() new_history.name = "imported: "+new_history.name new_history.user_id = user.id - new_history.add_galaxy_session(trans.get_galaxy_session( create=True )) + galaxy_session = trans.get_galaxy_session() + try: + association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + except: + association = None + new_history.add_galaxy_session( galaxy_session, association=association ) new_history.flush() if not user_history.datasets: trans.set_history( new_history ) @@ -461,7 +465,12 @@ class RootController( BaseController ): new_history = import_history.copy() new_history.name = "imported: "+new_history.name new_history.user_id = None - new_history.add_galaxy_session(trans.get_galaxy_session( create=True )) + galaxy_session = trans.get_galaxy_session() + try: + association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + except: + association = None + new_history.add_galaxy_session( galaxy_session, association=association ) new_history.flush() trans.set_history( new_history ) trans.log_event( "History imported, id: %s, name: '%s': " % (str(new_history.id) , new_history.name ) ) @@ -481,7 +490,12 @@ class RootController( BaseController ): else: new_history = trans.app.model.History.get( id ) if new_history: - new_history.add_galaxy_session(trans.get_galaxy_session( create=True )) + galaxy_session = trans.get_galaxy_session() + try: + association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + except: + association = None + new_history.add_galaxy_session( galaxy_session, association=association ) new_history.flush() trans.set_history( new_history ) trans.log_event( "History switched to id: %s, name: '%s'" % (str(new_history.id), new_history.name ) ) diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 5cc134c1350..79fa1e38b73 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -90,15 +90,11 @@ class User( BaseController ): .add_text( "email", "Email address", value=email, error=email_error ) .add_password( "password", "Password", value='', error=password_error, help="Forgot password? Reset here" % web.url_for( action='reset_password' ) ) ) - @web.expose def logout( self, trans ): + # Since logging an event requires a session, we'll log prior to ending the session trans.log_event( "User logged out" ) - # If the current history is saved for the current user it should be disconnected. - if trans.history.user == trans.user: - trans.set_history( None ) - trans.set_user( None ) - trans.end_galaxy_session() + new_galaxy_session = trans.logout_galaxy_session() return trans.show_ok_message( "You are no longer logged in", refresh_frames=['masthead', 'history'] ) @web.expose diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 6c3911c7a3a..954de968b45 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -108,7 +108,6 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): # that the current history should not be used for parameter values # and such). self.workflow_building_mode = False - @property def sa_session( self ): """ @@ -117,7 +116,6 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): to allow migration toward a more SQLAlchemy 0.4 style of use. """ return self.app.model.context.current - def log_event( self, message, tool_id=None, **kwargs ): """ Application level logging. Still needs fleshing out (log levels and @@ -138,119 +136,196 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): self.ensure_valid_galaxy_session() event.session_id = self.galaxy_session.id event.flush() - - def get_cookie( self, name='universe' ): - """ - Convienience method for getting the universe cookie - """ + def get_cookie( self, name='galaxysession' ): + """Convienience method for getting the galaxysession cookie""" try: - # If we've changed the cookie during the request return the new - # value + # If we've changed the cookie during the request return the new value if name in self.response.cookies: return self.response.cookies[name].value else: return self.request.cookies[name].value - except Exception: + except: return None - - def set_cookie( self, value, name='universe', path='/', age=90, version='1' ): - """ - Convienience method for setting the universe cookie - """ + def set_cookie( self, value, name='galaxysession', path='/', age=90, version='1' ): + """Convienience method for setting the galaxysession cookie""" + # The galaxysession cookie value must be a high entropy 128 bit random number encrypted + # using a server secret key. Any other value is invalid and could pose security issues. self.response.cookies[name] = value - self.response.cookies[name]['path'] = path - self.response.cookies[name]['max-age'] = 3600 * 24 * age - tstamp = time.localtime ( time.time() + 3600 * 24 * age ) - self.response.cookies[name]['expires'] = time.strftime('%a, %d-%b-%Y %H:%M:%S GMT', tstamp) + self.response.cookies[name]['path'] = path + self.response.cookies[name]['max-age'] = 3600 * 24 * age # 90 days + tstamp = time.localtime ( time.time() + 3600 * 24 * age ) + self.response.cookies[name]['expires'] = time.strftime( '%a, %d-%b-%Y %H:%M:%S GMT', tstamp ) self.response.cookies[name]['version'] = version - def get_history( self, create=False ): - """ - Load the current history - """ + """Load the current history""" if self.__history is NOT_SET: - history = None - id = self.get_cookie( name='universe' ) - if id: - history = self.app.model.History.get( id ) - if history is None or history.deleted: - history = self.new_history() - self.__history = history - if create is True and ( history is None or history.deleted ): - history = self.new_history() - return self.__history - + self.__history = None + # See if we have a galaxysession cookie + secure_id = self.get_cookie( name='galaxysession' ) + if secure_id: + session_key = self.security.decode_session_key( secure_id ) + try: + galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + if galaxy_session and galaxy_session.is_valid and galaxy_session.current_history_id: + history = self.app.model.History.get( galaxy_session.current_history_id ) + if history and not history.deleted: + self.__history = history + except Exception, e: + # This should only occur in development if the cookie is not synced with the db + pass + else: + # See if we have a deprecated universe cookie + # TODO: this should be eliminated some time after October 1, 2008 + # We'll keep it until then because the old universe cookies are valid for 90 days + history_id = self.get_cookie( name='universe' ) + if history_id: + history = self.app.model.History.get( int( history_id ) ) + if history and not history.deleted: + self.__history = history + # Expire the universe cookie since it is deprecated + self.set_cookie( name='universe', value=id, age=0 ) + if self.__history is None: + return self.new_history() + if create is True and self.__history is None: + return self.new_history() + return self.__history def new_history( self ): history = self.app.model.History() - """ - We are associating the last used genome_build with histories, so we will always - initialize a new history with the first dbkey in util.dbnames which is currently - ? unspecified (?) - """ - history.genome_build = util.dbnames.default_value - if history.user_id is None and self.user is not None: - history.user_id = self.user.id - if self.galaxy_session_is_valid(): - history.add_galaxy_session(self.get_galaxy_session()) + # Make sure we have an id history.flush() - self.set_cookie( name='universe', value=history.id ) + # Immediately associate the new history with self self.__history = history - return history - - def set_history( self, history ): - if history is None or history.deleted: - self.set_cookie( name='universe', value='' ) + # Make sure we have a valid session to associate with the new history + if self.galaxy_session_is_valid(): + galaxy_session = self.get_galaxy_session() else: - self.set_cookie( name='universe', value=history.id ) + galaxy_session = self.new_galaxy_session() + # We are associating the last used genome_build with histories, so we will always + # initialize a new history with the first dbkey in util.dbnames which is currently + # ? unspecified (?) + history.genome_build = util.dbnames.default_value + if self.user: + history.user_id = self.user.id + galaxy_session.user_id = self.user.id + try: + # See if we have already associated the history with the session + association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=history.id )[0] + except: + association = None + history.add_galaxy_session( galaxy_session, association=association ) + history.flush() + galaxy_session.current_history_id = history.id + galaxy_session.flush() + self.__history = history + return self.__history + def set_history( self, history ): + if history and not history.deleted and self.galaxy_session_is_valid(): + galaxy_session = self.get_galaxy_session() + galaxy_session.current_history_id = history.id + galaxy_session.flush() self.__history = history history = property( get_history, set_history ) - def get_user( self ): - """ - Return the current user if logged in (based on cookie) or `None`. - """ + """Return the current user if logged in or None.""" if self.__user is NOT_SET: - id = self.get_cookie( name='universe_user' ) - if not id: - self.__user = None + self.__user = None + # See if we have a galaxysession cookie + secure_id = self.get_cookie( name='galaxysession' ) + if secure_id: + session_key = self.security.decode_session_key( secure_id ) + try: + galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + if galaxy_session and galaxy_session.is_valid and galaxy_session.user_id: + user = self.app.model.User.get( galaxy_session.user_id ) + if user: + self.__user = user + except: + # This should only occur in development if the cookie is not synced with the db + pass else: - self.__user = self.app.model.User.get( int( id ) ) + # See if we have a deprecated universe_user cookie + # TODO: this should be eliminated some time after October 1, 2008 + # We'll keep it until then because the old universe cookies are valid for 90 days + user_id = self.get_cookie( name='universe_user' ) + if user_id: + user = self.app.model.User.get( int( user_id ) ) + if user: + self.__user = user + # Expire the universe_user cookie since it is deprecated + self.set_cookie( name='universe_user', value='', age=0 ) return self.__user - def set_user( self, user ): - """ - Set the current user to `user` (by setting a cookie). - """ - if user is None: - self.set_cookie( name='universe_user', value='' ) - else: - self.set_cookie( name='universe_user', value=user.id ) + """Set the current user if logged in.""" + if user is not None and self.galaxy_session_is_valid(): + galaxy_session = self.get_galaxy_session() + if galaxy_session.user_id != user.id: + galaxy_session.user_id = user.id + galaxy_session.flush() self.__user = user user = property( get_user, set_user ) - def get_galaxy_session( self, create=False ): - # Return the current user's galaxy_session. + """Return the current user's GalaxySession""" if self.__galaxy_session is NOT_SET: - id = self.get_cookie( name='universe_session' ) - if not id: - self.__galaxy_session = None + self.__galaxy_session = None + # See if we have a galaxysession cookie + secure_id = self.get_cookie( name='galaxysession' ) + if secure_id: + # Decode the cookie value to get the session_key + session_key = self.security.decode_session_key( secure_id ) + try: + # Retrive the galaxy_session id via the unique session_key + galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + if galaxy_session and galaxy_session.is_valid: + self.__galaxy_session = galaxy_session + except: + # This should only occur in development if the cookie is not synced with the db + pass else: - self.__galaxy_session = self.app.model.GalaxySession.get( int( id ) ) + # See if we have a deprecated universe_session cookie + # TODO: this should be eliminated some time after October 1, 2008 + # We'll keep it until then because the old universe cookies are valid for 90 days + session_id = self.get_cookie( name='universe_session' ) + if session_id: + galaxy_session = self.app.model.GalaxySession.get( int( session_id ) ) + # NOTE: We can't test for is_valid here since the old session records did not include this flag + if galaxy_session: + # Set the new galaxysession cookie value, old session records did not have a session_key or is_valid flag + session_key = self.security.get_new_session_key() + galaxy_session.session_key = session_key + galaxy_session.is_valid = True + galaxy_session.flush() + secure_id = self.security.encode_session_key( session_key ) + self.set_cookie( name='galaxysession', value=secure_id ) + # Expire the universe_user cookie since it is deprecated + self.set_cookie( name='universe_session', value='', age=0 ) + self.__galaxy_session = galaxy_session if create is True and self.__galaxy_session is None: - galaxy_session = self.new_galaxy_session() + return self.new_galaxy_session() return self.__galaxy_session - - def new_galaxy_session( self ): - # Create a new galaxy_session, retrieving the user's most recently updated history - galaxy_session = self.app.model.GalaxySession() - if self.user is not None: + def new_galaxy_session( self, prev_session_id=None ): + """Create a new secure galaxy_session""" + session_key = self.security.get_new_session_key() + galaxy_session = self.app.model.GalaxySession( session_key=session_key, is_valid=True, prev_session_id=prev_session_id ) + # Make sure we have an id + galaxy_session.flush() + # Immediately associate the new session with self + self.__galaxy_session = galaxy_session + if prev_session_id is not None: + # User logged out, so we need to create a new history for this session + self.history = self.new_history() + galaxy_session.current_history_id = self.history.id + elif self.user is not None: galaxy_session.user_id = self.user.id + # Set this session's current_history_id to the user's last updated history h = self.app.model.History ht = h.table where = ( ht.c.user_id==self.user.id ) & ( ht.c.deleted=='f' ) history = h.query().filter( where ).order_by( desc( ht.c.update_time ) ).first() - if history is not None: + if history: self.history = history + galaxy_session.current_history_id = self.history.id + elif self.history: + galaxy_session.current_history_id = self.history.id galaxy_session.remote_host = self.request.remote_host galaxy_session.remote_addr = self.request.remote_addr try: @@ -258,53 +333,60 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): except: galaxy_session.referer = None if self.history is not None: - galaxy_session.add_history(self.history) + # See if we have already associated the session with the history + try: + association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=self.history.id )[0] + except: + association = None + galaxy_session.add_history( self.history, association=association ) galaxy_session.flush() - self.set_cookie( name='universe_session', value=galaxy_session.id ) + # Set the cookie value to the encrypted session_key + self.set_cookie( name='galaxysession', value=self.security.encode_session_key( session_key ) ) self.__galaxy_session = galaxy_session return self.__galaxy_session - def set_galaxy_session( self, galaxy_session ): - # Set the current galaxy_session by setting the universe_session cookie. - if galaxy_session is None: - #TODO we may want to raise an exception here instead of creating a new galaxy_session - galaxy_session = self.new_galaxy_session() - else: - if galaxy_session.user_id is None and self.user is not None: - galaxy_session.user_id = self.user.id - galaxy_session.flush() - self.set_cookie( name='universe_session', value=galaxy_session.id ) - self.__galaxy_session = galaxy_session - - def galaxy_session_is_valid( self ): - # TODO do we want better validation here? - valid = False - galaxy_session = self.get_galaxy_session() - if galaxy_session is not None and galaxy_session.id is not None: - valid = True - return valid - - def ensure_valid_galaxy_session( self ): - if not self.galaxy_session_is_valid(): - self.new_galaxy_session() - - def end_galaxy_session( self ): - # End the current galaxy_session by expiring the universe_session cookie. - if self.galaxy_session_is_valid(): - self.set_cookie( name='universe_session', value=self.galaxy_session.id, age=0 ) - self.__galaxy_session = None + """Set the current galaxy_session""" + self.__galaxy_session = galaxy_session galaxy_session = property( get_galaxy_session, set_galaxy_session ) - - def make_associations( self ): + def galaxy_session_is_valid( self ): + try: + return self.galaxy_session.is_valid + except: + return False + def ensure_valid_galaxy_session( self ): + """Make sure we have a valid galaxy session, create a new one if necessary.""" + if not self.galaxy_session_is_valid(): + galaxy_session = self.new_galaxy_session() + def logout_galaxy_session( self ): + """ + Logout the current user by setting user to None and galaxy_session.is_valid to False + in the db. A new galaxy_session is automatically created with prev_session_id is set + to save a reference to the current one as a way of chaining them together + """ if self.galaxy_session_is_valid(): - if self.galaxy_session.user_id is None and self.user is not None: - self.galaxy_session.user_id = self.user.id - self.galaxy_session.flush() - self.__galaxy_session = self.galaxy_session - if self.history is not None and self.user is not None: - self.history.user_id = self.user.id - self.history.flush() - self.__history = self.history + galaxy_session = self.get_galaxy_session() + old_session_id = galaxy_session.id + galaxy_session.is_valid = False + galaxy_session.flush() + self.set_user( None ) + return self.new_galaxy_session( prev_session_id=old_session_id ) + else: + error( "Attempted to logout an invalid galaxy_session" ) + def make_associations( self ): + history = self.get_history() + user = self.get_user() + if self.galaxy_session_is_valid(): + galaxy_session = self.get_galaxy_session() + if galaxy_session.user_id is None and user is not None: + galaxy_session.user_id = user.id + if history is not None: + galaxy_session.current_history_id = history.id + galaxy_session.flush() + self.__galaxy_session = galaxy_session + if history is not None and user is not None: + history.user_id = user.id + history.flush() + self.__history = history def get_toolbox(self): """Returns the application toolbox""" diff --git a/lib/galaxy/web/security/__init__.py b/lib/galaxy/web/security/__init__.py index 93a52885028..0a83eda3fe4 100644 --- a/lib/galaxy/web/security/__init__.py +++ b/lib/galaxy/web/security/__init__.py @@ -2,11 +2,15 @@ import pkg_resources pkg_resources.require( "pycrypto" ) from Crypto.Cipher import Blowfish +from Crypto.Util.randpool import RandomPool +from Crypto.Util import number class SecurityHelper( object ): + # TODO: checking if histories/datasets are owned by the current user) will be moved here. def __init__( self, **config ): self.id_secret = config['id_secret'] self.id_cipher = Blowfish.new( self.id_secret ) + self.__random_pool = RandomPool( 1024 ) def encode_id( self, id ): # Convert to string s = str( id ) @@ -15,4 +19,22 @@ class SecurityHelper( object ): # Encrypt return self.id_cipher.encrypt( s ).encode( 'hex' ) def decode_id( self, id ): - return int( self.id_cipher.decrypt( id.decode( 'hex' ) ).lstrip( "!" ) ) \ No newline at end of file + return int( self.id_cipher.decrypt( id.decode( 'hex' ) ).lstrip( "!" ) ) + def encode_session_key( self, session_key ): + # Session keys are strings + # Pad to a multiple of 8 with leading "!" + s = ( "!" * ( 8 - len( session_key ) % 8 ) ) + session_key + # Encrypt + return self.id_cipher.encrypt( s ).encode( 'hex' ) + def decode_session_key( self, session_key ): + # Session keys are strings + return self.id_cipher.decrypt( session_key.decode( 'hex' ) ).lstrip( "!" ) + def get_new_session_key( self ): + # Generate a unique, high entropy 128 bit random number + while self.__random_pool.entropy < 100: + self.__random_pool.add_event() + self.__random_pool.stir() + rn = number.getRandomNumber( 128, self.__random_pool.get_bytes ) + # session_key must be a string + return str( rn ) + \ No newline at end of file diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index a5b262d436b..464eb957162 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -61,6 +61,9 @@ session_data_dir = %(here)s/database/beaker_sessions session_key = galaxysessions session_secret = changethisinproduction +# Galaxy session security +id_secret = changethisinproductiontoo + # Configuration for debugging middleware debug = true use_lint = false