Introducing basic session security - REQUIRES DATABASE SCHEMA CHANGE and REQUIRES CONFIG CHANGE.

SQL commands for schema changes:

ALTER TABLE galaxy_session ADD COLUMN current_history_id INTEGER;
ALTER TABLE galaxy_session ADD FOREIGN KEY (current_history_id) REFERENCES history(id);
ALTER TABLE galaxy_session ADD COLUMN session_key VARCHAR(255) UNIQUE;
ALTER TABLE galaxy_session ADD COLUMN is_valid BOOLEAN DEFAULT false;
ALTER TABLE galaxy_session ADD COLUMN prev_session_id INTEGER;

Galaxy config change:

In the [app:main] section, add:

# Galaxy session security
id_secret = changethisinproductiontoo

1) The "universe", "universe_user" and "universe_session" cookies are deprecated and replaced with 1 new cookie named galaxysession".  The deprecated cookies are still supported and will be for at least 90 days, but when a valid one is found, the value is taken and used to create a new "galaxysession" cookie and the deprecated cookie is immediately expired.

2) The value of the new "galaxysession" cookie is an encrypted unique, high entropy 128 bit random number.  The decoded value is the value stored in the new session_key column of the galaxy_session table.

3) Whenever a user logs in or out they get a new GalaxySession, and the old one in the database is marked as invalidated, so a compromised cookie won't survive across login/logout. Also, when creating the new session, a reference to the previous one is saved ( via the new prev_session_id column in the galaxy_session table ) so we have a way to chain them together.
This commit is contained in:
Greg Von Kuster
2008-06-19 14:13:00 +00:00
parent ecdfb6d3c0
commit 3443e25e06
7 changed files with 274 additions and 139 deletions
+19 -6
View File
@@ -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 ):
+6 -1
View File
@@ -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 ),
+18 -4
View File
@@ -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 ) )
+2 -6
View File
@@ -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="<a href='%s'>Forgot password? Reset here</a>" % 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
+203 -121
View File
@@ -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"""
+23 -1
View File
@@ -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( "!" ) )
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 )
+3
View File
@@ -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