diff --git a/LICENSE.txt b/LICENSE.txt index 69978e57efb..23e45c00eae 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -17,4 +17,9 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Some icons found in Galaxy are from the Silk Icons set, available under +the Creative Commons Attribution 2.5 License, from: + +http://www.famfamfam.com/lab/icons/silk/ diff --git a/eggs.ini b/eggs.ini index 6b918a7f324..1950ce4aa05 100644 --- a/eggs.ini +++ b/eggs.ini @@ -39,7 +39,7 @@ PasteDeploy = 1.3.1 PasteScript = 1.3.6 Routes = 1.6.3 simplejson = 1.5 -SQLAlchemy = 0.3.11 +SQLAlchemy = 0.4.7p1 Tempita = 0.1 twill = 0.9 WebError = 0.8a @@ -84,7 +84,7 @@ PasteDeploy = http://cheeseshop.python.org/packages/source/P/PasteDeploy/PasteDe PasteScript = http://cheeseshop.python.org/packages/source/P/PasteScript/PasteScript-1.3.6.tar.gz Routes = http://pypi.python.org/packages/source/R/Routes/Routes-1.6.3.tar.gz simplejson = http://cheeseshop.python.org/packages/source/s/simplejson/simplejson-1.5.tar.gz -SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.3.11.tar.gz +SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.4.7p1.tar.gz Tempita = http://pypi.python.org/packages/source/T/Tempita/Tempita-0.1.tar.gz twill = http://darcs.idyll.org/~t/projects/twill-0.9.tar.gz WebError = http://pypi.python.org/packages/source/W/WebError/WebError-0.8a.tar.gz diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 0c4ea728395..eb26d6a9acd 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -4,6 +4,7 @@ from galaxy import config, jobs, util, tools, web import galaxy.model import galaxy.model.mapping import galaxy.datatypes.registry +import galaxy.security class UniverseApplication( object ): """Encapsulates the state of a Universe application""" @@ -20,7 +21,7 @@ class UniverseApplication( object ): if self.config.database_connection: db_url = self.config.database_connection else: - db_url = "sqlite://%s?isolation_level=IMMEDIATE" % self.config.database + db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database # Setup the database engine and ORM self.model = galaxy.model.mapping.init( self.config.file_path, db_url, @@ -30,6 +31,8 @@ class UniverseApplication( object ): self.toolbox = tools.ToolBox( self.config.tool_config, self.config.tool_path, self ) #Load datatype converters self.datatypes_registry.load_datatype_converters( self.toolbox ) + #Load security policy + self.security_agent = self.model.security_agent # Start the job queue job_dispatcher = jobs.DefaultJobDispatcher( self ) self.job_queue = jobs.JobQueue( self, job_dispatcher ) diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 4ef363a1f32..a54c0040c3c 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -45,7 +45,7 @@ class Configuration( object ): self.job_scheduler_policy = kwargs.get("job_scheduler_policy", "FIFO") self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") ) self.job_working_directory = resolve_path( kwargs.get( "job_working_directory", "database/job_working_directory" ), self.root ) - self.admin_pass = kwargs.get('admin_pass',"galaxy") + self.admin_users = kwargs.get( "admin_users", "" ) self.sendmail_path = kwargs.get('sendmail_path',"/usr/sbin/sendmail") self.mailing_join_addr = kwargs.get('mailing_join_addr',"galaxy-user-join@bx.psu.edu") self.error_email_to = kwargs.get( 'error_email_to', None ) @@ -64,6 +64,9 @@ class Configuration( object ): self.bugs_email = kwargs.get( 'bugs_email', None ) self.blog_url = kwargs.get( 'blog_url', None ) self.screencasts_url = kwargs.get( 'screencasts_url', None ) + self.library_import_dir = kwargs.get( 'library_import_dir', None ) + if self.library_import_dir is not None and not os.path.exists( self.library_import_dir ): + raise ConfigurationError( "library_import_dir specified in config (%s) does not exist" % self.library_import_dir ) # Parse global_conf and save the parser global_conf = kwargs.get( 'global_conf', None ) global_conf_parser = ConfigParser.ConfigParser() diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py index 071e7ce53a7..53a8bca7ba0 100644 --- a/lib/galaxy/datatypes/data.py +++ b/lib/galaxy/datatypes/data.py @@ -47,6 +47,9 @@ class Data( object ): """Stores the set of display applications, and viewing methods, supported by this datatype """ supported_display_apps = {} + """If False, the peek is regenerated whenever a dataset of this type is copied""" + copy_safe_peek = True + def __init__(self, **kwd): """Initialize the datatype""" object.__init__(self, **kwd) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 90d44ab47f4..78bf6964e81 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -5,7 +5,7 @@ Image classes import data import logging from galaxy.datatypes.sniff import * -from urllib import urlencode +from urllib import urlencode, quote_plus import zipfile log = logging.getLogger(__name__) @@ -103,18 +103,22 @@ def create_applet_tag_peek( class_name, archive, params ): class Gmaj( data.Data ): """Class describing a GMAJ Applet""" file_ext = "gmaj.zip" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, - "buttonlabel": "Launch GMAJ", - "nobutton": "false", - "urlpause" :"100", - "debug": "false", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey } ) - } - class_name = "edu.psu.bx.gmaj.MajApplet.class" - archive = "/static/gmaj/gmaj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, + "buttonlabel": "Launch GMAJ", + "nobutton": "false", + "urlpause" :"100", + "debug": "false", + "posturl": quote_plus( "history_add_to?%s" % "&".join( [ "%s=%s" % ( key, value ) for key, value in { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id }.items() ] ) ) + } + class_name = "edu.psu.bx.gmaj.MajApplet.class" + archive = "/static/gmaj/gmaj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the GMAJ applet." dataset.blurb = 'GMAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: @@ -175,17 +179,21 @@ class Html( data.Text ): class Laj( data.Text ): """Class describing a LAJ Applet""" file_ext = "laj" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "alignfile1": "display?id=%s" % dataset.id, - "buttonlabel": "Launch LAJ", - "title": "LAJ in Galaxy", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey } ), - "noseq": "true" - } - class_name = "edu.psu.cse.bio.laj.LajApplet.class" - archive = "/static/laj/laj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "alignfile1": "display?id=%s" % dataset.id, + "buttonlabel": "Launch LAJ", + "title": "LAJ in Galaxy", + "posturl": quote_plus( "history_add_to?%s" % "&".join( [ "%s=%s" % ( key, value ) for key, value in { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id }.items() ] ) ), + "noseq": "true" + } + class_name = "edu.psu.cse.bio.laj.LajApplet.class" + archive = "/static/laj/laj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the LAJ applet." dataset.blurb = 'LAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 3226cff095f..cbeac083a1d 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -13,6 +13,7 @@ from galaxy import util import tempfile import galaxy.datatypes.registry from galaxy.datatypes.metadata import MetadataCollection +from galaxy.security import RBACAgent import logging log = logging.getLogger( __name__ ) @@ -33,13 +34,14 @@ class User( object ): self.external = False # Relationships self.histories = [] + def set_password_cleartext( self, cleartext ): """Set 'self.password' to the digest of 'cleartext'.""" self.password = sha.new( cleartext ).hexdigest() def check_password( self, cleartext ): """Check if 'cleartext' matches 'self.password' when hashed.""" return self.password == sha.new( cleartext ).hexdigest() - + class Job( object ): """ A job represents a request to run a tool given input datasets, tool @@ -101,31 +103,69 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -class HistoryDatasetAssociation( object ): - def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, - dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, - parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): - self.name = name or "Unnamed dataset" - self.id = id - self.hid = hid - self.info = info - self.blurb = blurb - self.peek = peek - self.extension = extension - self.designation = designation - self.metadata = metadata or dict() - self.dbkey = dbkey - self.deleted = deleted - self.visible = visible - # Relationships - self.history = history - if not dataset and create_dataset: - dataset = Dataset() - dataset.flush() +class GroupDatasetAssociation( object ): + def __init__( self, group, dataset, permitted_actions=[] ): + if isinstance( group, GroupDatasetAssociation ) or \ + isinstance( group, DefaultUserGroupAssociation ) or \ + isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.group = group + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset self.dataset = dataset - self.parent_id = parent_id - self.validation_errors = validation_errors - self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.permitted_actions = permitted_actions + def add_permitted_action( self, action ): + if action not in self.permitted_actions: + return self.permitted_actions.append( action ) + raise 'action (%s) already exists in permitted actions list (%s: %s).' % ( action, str( self.id ), str( self.permitted_actions ) ) + def remove_permitted_action( self, action ): + return self.permitted_actions.remove( action ) + +class Group( object ): + public_id = None + permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) + def __init__( self, name = None, priority = 0 ): + self.name = name + self.priority = priority + @classmethod + def get_public_group( cls ): + return Group.get( cls.public_id ) + @classmethod + def set_public_group( cls, group ): + # We store the id instead of the object, because of alchemy sessions + if isinstance( group, Group ): + group = group.id + cls.public_id = group + @classmethod + def guess_public_group( cls ): + # Retrieve from database and store public group id + group = Group.select_by( name='public' )[0] + cls.set_public_group( group ) + +class UserGroupAssociation( object ): + def __init__( self, user, group ): + self.user = user + self.group = group + +class DefaultUserGroupAssociation( object ): + def __init__( self, user, group, permitted_actions ): + if isinstance( group, GroupDatasetAssociation ) or \ + isinstance( group, DefaultUserGroupAssociation ) or \ + isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.user = user + self.group = group + self.permitted_actions = permitted_actions + +class DefaultHistoryGroupAssociation( object ): + def __init__( self, history, group, permitted_actions ): + if isinstance( group, GroupDatasetAssociation ) or \ + isinstance( group, DefaultUserGroupAssociation ) or \ + isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.history = history + self.group = group + self.permitted_actions = permitted_actions @property def ext( self ): @@ -276,8 +316,6 @@ class HistoryDatasetAssociation( object ): for child in self.children: child.mark_deleted() - - class History( object ): def __init__( self, id=None, name=None, user=None ): self.id = id @@ -358,6 +396,7 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) + permitted_actions = galaxy.security.get_permitted_actions( 'DATASET' ) file_path = "/tmp/" engine = None def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): @@ -442,8 +481,364 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) -class Old_Dataset( Dataset ): - pass +class DatasetInstance( object ): + """A base class for all 'dataset instances', HDAs, LDAs, etc""" + states = Dataset.states + permitted_actions = Dataset.permitted_actions + def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, + dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, + parent_id=None, validation_errors=None, visible=True, create_dataset = False ): + self.name = name or "Unnamed dataset" + self.id = id + self.info = info + self.blurb = blurb + self.peek = peek + self.extension = extension + self.dbkey = dbkey + self.designation = designation + self.metadata = metadata or dict() + self.deleted = deleted + self.visible = visible + # Relationships + if not dataset and create_dataset: + dataset = Dataset() + dataset.flush() + self.dataset = dataset + self.parent_id = parent_id + self.validation_errors = validation_errors + @property + def ext( self ): + return self.extension + def get_dataset_state( self ): + return self.dataset.state + def set_dataset_state ( self, state ): + self.dataset.state = state + self.dataset.flush() #flush here, because hda.flush() won't flush the Dataset object + state = property( get_dataset_state, set_dataset_state ) + def get_file_name( self ): + return self.dataset.get_file_name() + def set_file_name (self, filename): + return self.dataset.set_file_name( filename ) + file_name = property( get_file_name, set_file_name ) + @property + def extra_files_path( self ): + return self.dataset.extra_files_path + @property + def datatype( self ): + return datatypes_registry.get_datatype_by_extension( self.extension ) + def get_metadata( self ): + if not hasattr( self, '_metadata_collection' ): + self._metadata_collection = MetadataCollection( self, self.datatype.metadata_spec ) + return self._metadata_collection + def set_metadata( self, bunch ): + # Needs to accept a MetadataCollection, a bunch, or a dict + self._metadata = dict( bunch.items() ) + metadata = property( get_metadata, set_metadata ) + # This provide backwards compatibility with using the old dbkey + # field in the database. That field now maps to "old_dbkey" (see mapping.py). + def get_dbkey( self ): + dbkey = self.metadata.dbkey + if not isinstance(dbkey, list): dbkey = [dbkey] + #if dbkey in [["?"], [None], []]: dbkey = [self.old_dbkey] + if dbkey in [[None], []]: return "?" + return dbkey[0] + def set_dbkey( self, value ): + if "dbkey" in self.datatype.metadata_spec: + if not isinstance(value, list): + self.metadata.dbkey = [value] + else: + self.metadata.dbkey = value + #if isinstance(value, list): + # self.old_dbkey = value[0] + #else: + # self.old_dbkey = value + dbkey = property( get_dbkey, set_dbkey ) + def change_datatype( self, new_ext ): + self.clear_associated_files() + if hasattr( self, '_metadata_collection' ): + del self._metadata_collection + datatypes_registry.change_datatype( self, new_ext ) + def get_size( self ): + """Returns the size of the data on disk""" + return self.dataset.get_size() + def set_size( self ): + """Returns the size of the data on disk""" + return self.dataset.set_size() + def has_data( self ): + """Detects whether there is any data""" + return self.dataset.has_data() + def get_raw_data( self ): + """Returns the full data. To stream it open the file_name and read/write as needed""" + return self.datatype.get_raw_data( self ) + def write_from_stream( self, stream ): + """Writes data from a stream""" + self.datatype.write_from_stream(self, stream) + def set_raw_data( self, data ): + """Saves the data on the disc""" + self.datatype.set_raw_data(self, data) + def get_mime( self ): + """Returns the mime type of the data""" + return datatypes_registry.get_mimetype_by_extension( self.extension.lower() ) + def set_peek( self ): + return self.datatype.set_peek( self ) + def init_meta( self, copy_from=None ): + return self.datatype.init_meta( self, copy_from=copy_from ) + def set_meta( self, **kwd ): + self.clear_associated_files( metadata_safe = True ) + return self.datatype.set_meta( self, **kwd ) + def set_readonly_meta( self, **kwd ): + return self.datatype.set_readonly_meta( self, **kwd ) + def missing_meta( self ): + return self.datatype.missing_meta( self ) + def as_display_type( self, type, **kwd ): + return self.datatype.as_display_type( self, type, **kwd ) + def display_peek( self ): + return self.datatype.display_peek( self ) + def display_name( self ): + return self.datatype.display_name( self ) + def display_info( self ): + return self.datatype.display_info( self ) + def get_converted_files_by_type( self, file_type ): + valid = [] + for assoc in self.implicitly_converted_datasets: + if not assoc.deleted and assoc.type == file_type: + valid.append( assoc.dataset ) + return valid + def clear_associated_files( self, metadata_safe = False, purge = False ): + raise 'Unimplemented' + def get_child_by_designation(self, designation): + for child in self.children: + if child.designation == designation: + return child + return None + def get_converter_types(self): + return self.datatype.get_converter_types( self, datatypes_registry) + def add_validation_error( self, validation_error ): + self.validation_errors.append( validation_error ) + def extend_validation_errors( self, validation_errors ): + self.validation_errors.extend(validation_errors) + def mark_deleted( self, include_children=True ): + self.deleted = True + if include_children: + for child in self.children: + child.mark_deleted() + +class HistoryDatasetAssociation( DatasetInstance ): + def __init__( self, + hid = None, + history = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.hid = hid + # Relationships + self.history = history + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + def copy( self, copy_children = False, parent_id = None, target_history = None ): + des = HistoryDatasetAssociation( hid=self.hid, + name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_history_dataset_association=self, + history = target_history ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def clear_associated_files( self, metadata_safe = False, purge = False ): + #metadata_safe = True means to only clear when assoc.metadata_safe == False + for assoc in self.implicitly_converted_datasets: + if not metadata_safe or not assoc.metadata_safe: + assoc.clear( purge = purge ) + +class History( object ): + def __init__( self, id=None, name=None, user=None ): + self.id = id + self.name = name or "Unnamed history" + self.deleted = False + self.purged = False + self.genome_build = None + # Relationships + self.user = user + self.datasets = [] + self.galaxy_sessions = [] + def _next_hid( self ): + # TODO: override this with something in the database that ensures + # better integrity + if len( self.datasets ) == 0: + return 1 + else: + last_hid = 0 + for dataset in self.datasets: + if dataset.hid > last_hid: + last_hid = dataset.hid + return last_hid + 1 + 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 isinstance( dataset, Dataset ): + dataset = HistoryDatasetAssociation( dataset = dataset ) + dataset.flush() + elif not isinstance( dataset, HistoryDatasetAssociation ): + raise TypeError, "You can only add Dataset and HistoryDatasetAssociation instances to a history." + if parent_id: + for data in self.datasets: + if data.id == parent_id: + dataset.hid = data.hid + break + else: + if set_hid: dataset.hid = self._next_hid() + else: + if set_hid: dataset.hid = self._next_hid() + dataset.history = self + if genome_build not in [None, '?']: + self.genome_build = genome_build + self.datasets.append( dataset ) + def copy( self, target_user = None ): + if not target_user: + target_user = self.user + des = History( user = target_user ) + des.flush() + des.name = self.name + for data in self.datasets: + new_data = data.copy( copy_children = True, target_history = des ) + des.add_dataset( new_data, set_hid = False ) + new_data.flush() + des.hid_counter = self.hid_counter + des.flush() + return des + +class Library( object ): + def __init__( self, name = None, description = None, root_folder = None ): + self.name = name or "Unnamed library" + self.description = description + self.root_folder = root_folder + +class LibraryFolder( object ): + def __init__( self, name = None, description = None, item_count = 0, order_id = None ): + self.name = name or "Unnamed folder" + self.description = description + self.item_count = item_count + self.order_id = order_id + self.genome_build = None + def add_dataset( self, dataset, genome_build=None ): + dataset.folder_id = self.id + dataset.order_id = self.item_count + self.item_count += 1 + if genome_build not in [None, '?']: + self.genome_build = genome_build + def add_folder( self, folder ): + folder.parent_id = self.id + folder.order_id = self.item_count + self.item_count += 1 + + @property + def active_components( self ): + return list( self.active_folders ) + list( self.active_datasets ) + +class LibraryFolderDatasetAssociation( DatasetInstance ): + def __init__( self, + folder = None, + order_id = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.folder = folder + self.order_id = order_id + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + def to_history_dataset_association( self, parent_id = None, target_history = None ): + if target_history: + hid = target_history._next_hid() + else: + hid = None + des = HistoryDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self, + history = target_history, + hid = hid ) + des.flush() + for child in self.children: + child_copy = child.to_history_dataset_association( parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def copy( self, copy_children = False, parent_id = None, target_folder = None ): + des = LibraryFolderDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self, + folder = target_folder ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def clear_associated_files( self, metadata_safe = False, purge = False ): + return + +class LibraryTag( object ): + def __init__( self, tag ): + self.tag = tag + +class LibraryTagFolderAssociation( object ): + def __init__( self, tag, folder ): + self.tag = tag + self.folder = folder + +class LibraryTagDatasetAssociation( object ): + def __init__( self, tag, dataset ): + self.tag = tag + self.dataset = dataset + +# class Query( object ): +# def __init__( self, name=None, state=None, tool_parameters=None, history=None ): +# self.name = name or "Unnamed query" +# self.state = state +# self.tool_parameters = tool_parameters +# # Relationships +# self.history = history +# self.datasets = [] + class ValidationError( object ): def __init__( self, message=None, err_type=None, attributes=None ): @@ -485,7 +880,16 @@ class Event( object ): self.message = message class GalaxySession( object ): - 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 ): + 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 @@ -532,7 +936,7 @@ class WorkflowStep( object ): self.tool_inputs = None self.tool_errors = None self.position = None - self.input_connections = None + self.input_connections = [] self.config = None class WorkflowStepConnection( object ): diff --git a/lib/galaxy/model/custom_types.py b/lib/galaxy/model/custom_types.py index f537a93f463..72d6b79075c 100644 --- a/lib/galaxy/model/custom_types.py +++ b/lib/galaxy/model/custom_types.py @@ -16,16 +16,15 @@ class JSONType( TypeDecorator ): self.mutable = mutable super( JSONType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_bind_param( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) - return self.jsonifyer.loads( str(buf) ) - - def convert_bind_param( self, value, dialect ): + return self.jsonifyer.dumps( value ) + + def process_result_value( self, value, dialect ): if value is None: return None - return self.impl.convert_bind_param( self.jsonifyer.dumps(value), dialect ) + return self.jsonifyer.loads( str( value ) ) def copy_value( self, value ): if self.mutable: @@ -58,10 +57,10 @@ class MetadataType( JSONType ): self.mutable = mutable super( MetadataType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_result_value( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) + buf = value ret = None try: ret = self.pickler.loads( str(buf) ) @@ -75,7 +74,7 @@ class MetadataType( JSONType ): class TrimmedString( TypeDecorator ): impl = String - def convert_bind_param( self, value, dialect ): + def process_bind_param( self, value, dialect ): """Automatically truncate string values""" if self.impl.length and value is not None: value = value[0:self.impl.length] diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cdc8419c439..222de251097 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -5,23 +5,21 @@ are encapsulated here. import logging log = logging.getLogger( __name__ ) -import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) - import sys import datetime -from sqlalchemy.ext.sessioncontext import SessionContext -from sqlalchemy.ext.assignmapper import assign_mapper -from sqlalchemy.ext.orderinglist import ordering_list - -from sqlalchemy import * from galaxy.model import * +from galaxy.model.orm import * +from galaxy.model.orm.ext.assignmapper import * from galaxy.model.custom_types import * from galaxy.util.bunch import Bunch +from galaxy.security import GalaxyRBACAgent -metadata = DynamicMetaData( threadlocal=False ) -context = SessionContext( create_session ) +metadata = MetaData() +context = Session = scoped_session( sessionmaker( autoflush=False, transactional=False ) ) + +# For backward compatibility with "context.current" +context.current = Session dialect_to_egg = { "sqlite" : "pysqlite>=2", @@ -65,7 +63,6 @@ History.table = Table( "history", metadata, # Column( "state", String( 64 ) ), # Column( "tool_parameters", Pickle() ) ) - HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata, Column( "id", Integer, primary_key=True ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), @@ -73,6 +70,7 @@ HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id" ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), Column( "hid", Integer ), Column( "name", TrimmedString( 255 ) ), Column( "info", TrimmedString( 255 ) ), @@ -114,21 +112,124 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) +Group.table = Table( "galaxy_group", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT, index=True, unique=True ), + Column( "priority", Integer ), + Column( "deleted", Boolean, index=True, default=False ) ) + +UserGroupAssociation.table = Table( "user_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "permitted_actions", JSONType(), default=[] ) ) + +# The following table stores the permissions that are considered the defaults for new histories when they are created by a user +DefaultUserGroupAssociation.table = Table( "default_user_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "permitted_actions", JSONType(), default=[] ) ) + +# The following table stores the default permissions assigned to histories for datasets +# that need permissions ( dataset permissions that cannot be determined based on ancestor ) +DefaultHistoryGroupAssociation.table = Table( "default_history_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "permitted_actions", JSONType(), default=[] ) ) + +LibraryFolderDatasetAssociation.table = Table( "library_folder_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "order_id", Integer ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id", use_alter=True, name='history_dataset_association_dataset_id_fkey' ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id", use_alter=True, name='library_folder_dataset_association_id_fkey' ), nullable=True ), + Column( "name", TrimmedString( 255 ) ), + Column( "info", TrimmedString( 255 ) ), + Column( "blurb", TrimmedString( 255 ) ), + Column( "peek" , TEXT ), + Column( "extension", TrimmedString( 64 ) ), + Column( "metadata", MetadataType(), key="_metadata" ), + Column( "parent_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), + Column( "designation", TrimmedString( 255 ) ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "visible", Boolean ) ) + +Library.table = Table( "library", metadata, + Column( "id", Integer, primary_key=True ), + Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT, index=True ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "description", TEXT ) ) + +LibraryFolder.table = Table( "library_folder", metadata, + Column( "id", Integer, primary_key=True ), + Column( "parent_id", Integer, ForeignKey( "library_folder.id" ), nullable = True, index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "description", TEXT ), + Column( "order_id", Integer ), + Column( "item_count", Integer ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "genome_build", TrimmedString( 40 ) ) ) + +LibraryTag.table = Table( "library_tag", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "text", TEXT ) ) + +LibraryTagFolderAssociation.table = Table( "library_tag_folder_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "library_tag.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +LibraryTagDatasetAssociation.table = Table( "library_tag_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "library_tag.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + Job.table = Table( "job", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), Column( "tool_id", String( 255 ) ), - Column( "tool_version", String, default="1.0.0" ), + Column( "tool_version", TEXT, default="1.0.0" ), Column( "state", String( 64 ) ), Column( "info", TrimmedString( 255 ) ), - Column( "command_line", String() ), + Column( "command_line", TEXT ), Column( "param_filename", String( 1024 ) ), Column( "runner_name", String( 255 ) ), - Column( "stdout", String() ), - Column( "stderr", String() ), - Column( "traceback", String() ), + Column( "stdout", TEXT ), + Column( "stderr", TEXT ), + Column( "traceback", TEXT ), Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True, nullable=True ), Column( "job_runner_name", String( 255 ) ), Column( "job_runner_external_id", String( 255 ) ) ) @@ -188,7 +289,7 @@ StoredWorkflow.table = Table( "stored_workflow", metadata, Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=False ), Column( "latest_workflow_id", Integer, ForeignKey( "workflow.id", use_alter=True, name='stored_workflow_latest_workflow_id_fk' ), index=True ), - Column( "name", String ), + Column( "name", TEXT ), Column( "deleted", Boolean, default=False ), ) @@ -197,7 +298,7 @@ Workflow.table = Table( "workflow", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True, nullable=False ), - Column( "name", String ), + Column( "name", TEXT ), Column( "has_cycles", Boolean ), Column( "has_errors", Boolean ) ) @@ -208,8 +309,8 @@ WorkflowStep.table = Table( "workflow_step", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "workflow_id", Integer, ForeignKey( "workflow.id" ), index=True, nullable=False ), Column( "type", String(64) ), - Column( "tool_id", String ), - Column( "tool_version", String ), # Reserved for future + Column( "tool_id", TEXT ), + Column( "tool_version", TEXT ), # Reserved for future Column( "tool_inputs", JSONType ), Column( "tool_errors", JSONType ), Column( "position", JSONType ), @@ -222,8 +323,8 @@ WorkflowStepConnection.table = Table( "workflow_step_connection", metadata, Column( "id", Integer, primary_key=True ), Column( "output_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), Column( "input_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), - Column( "output_name", String ), - Column( "input_name", String) + Column( "output_name", TEXT ), + Column( "input_name", TEXT) ) StoredWorkflowUserShareAssociation.table = Table( "stored_workflow_user_share_connection", metadata, @@ -248,27 +349,32 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab dataset=relation( Dataset, primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ), lazy=False ), - history=relation( - History, - primaryjoin=( History.table.c.id == HistoryDatasetAssociation.table.c.history_id ) ), + # .history defined in History mapper copied_to_history_dataset_associations=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id], uselist=False ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), children=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ) + backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ) ) ) assign_mapper( context, Dataset, Dataset.table, properties=dict( history_associations=relation( HistoryDatasetAssociation, - primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ) + primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ), + library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) ) ) ) @@ -298,6 +404,84 @@ assign_mapper( context, User, User.table, collection_class=ordering_list( 'order_index' ) ) ) ) +assign_mapper( context, Group, Group.table, + properties=dict( users=relation( UserGroupAssociation ), + datasets=relation( GroupDatasetAssociation ) ) ) + +assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, + properties=dict( user=relation( User, backref = "groups" ), + group=relation( Group, backref = "members" ) ) ) + +assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "groups" ), + group=relation( Group, backref = "group_datasets" ) ) ) + +assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, + properties=dict( user=relation( User, backref = "default_groups" ), + group=relation( Group ) ) ) + +assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssociation.table, + properties=dict( history=relation( History, backref = "default_groups" ), + group=relation( Group ) ) ) + +assign_mapper( context, Library, Library.table, + properties=dict( + root_folder=relation( LibraryFolder, + backref = backref( "library_root" ) ) + ) ) + +assign_mapper( context, LibraryFolder, LibraryFolder.table, + properties=dict( + folders=relation( + LibraryFolder, + primaryjoin=( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ), remote_side=[LibraryFolder.table.c.id] ) ), + active_folders=relation( LibraryFolder, + primaryjoin=( ( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ) & ( not_( LibraryFolder.table.c.deleted ) ) ), + order_by=asc( LibraryFolder.table.c.order_id ), + lazy=True, #"""sqlalchemy.exceptions.ArgumentError: Error creating eager relationship 'active_folders' on parent class '' to child class '': Cant use eager loading on a self referential relationship.""" + viewonly=True ), + active_datasets=relation( LibraryFolderDatasetAssociation, + primaryjoin=( ( LibraryFolderDatasetAssociation.table.c.folder_id == LibraryFolder.table.c.id ) & ( not_( LibraryFolderDatasetAssociation.table.c.deleted ) ) ), + order_by=asc( LibraryFolderDatasetAssociation.table.c.order_id ), + lazy=False, + viewonly=True ), + tags=relation( + LibraryTagFolderAssociation, + primaryjoin=( LibraryFolder.table.c.id == LibraryTagFolderAssociation.table.c.folder_id ), + backref=backref( "folders" ) ) + ) ) + +assign_mapper( context, LibraryFolderDatasetAssociation, LibraryFolderDatasetAssociation.table, + properties=dict( + dataset=relation( Dataset ), + folder=relation( + LibraryFolder, + backref=backref( "datasets" ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_library_folder_dataset_association", primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + children=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + tags=relation( + LibraryTagDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.id == LibraryTagDatasetAssociation.table.c.dataset_id ), + backref=backref( "datasets" ) ) + ) ) + +assign_mapper( context, LibraryTag, LibraryTag.table ) + +assign_mapper( context, LibraryTagFolderAssociation, LibraryTagFolderAssociation.table, + properties=dict( tag=relation( LibraryTag ), + folder=relation( LibraryFolder ) ) ) + +assign_mapper( context, LibraryTagDatasetAssociation, LibraryTagDatasetAssociation.table, + properties=dict( tag=relation( LibraryTag ), + dataset=relation( LibraryFolderDatasetAssociation ) ) ) + assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -366,11 +550,12 @@ def db_next_hid( self ): Override __next_hid to generate from the database in a concurrency safe way. """ - conn = self.table.engine.contextual_connect() + conn = object_session( self ).connection() + table = self.table trans = conn.begin() try: - next_hid = select( [self.c.hid_counter], self.c.id == self.id, for_update=True ).scalar() - self.table.update( self.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) trans.commit() return next_hid except: @@ -399,18 +584,31 @@ def init( file_path, url, engine_options={}, create_tables=False ): # Create the database engine engine = create_engine( url, **engine_options ) # Connect the metadata to the database. - metadata.connect( engine ) - ## metadata.engine.echo = True + metadata.bind = engine + # Clear any existing contextual sessions and reconfigure + Session.remove() + Session.configure( bind=engine ) # Create tables if needed if create_tables: metadata.create_all() # metadata.engine.commit() # Pack everything into a bunch result = Bunch( **globals() ) - result.engine = metadata.engine - result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) - result.context = context + result.engine = engine + result.flush = lambda *args, **kwargs: Session.flush( *args, **kwargs ) + result.session = Session + # For backward compatibility with "model.context.current" + result.context = Session result.create_tables = create_tables + #load local galaxy security policy + result.security_agent = GalaxyRBACAgent( result ) + # Ensure group named 'public' exists + public_group = result.Group.get_by( name='public' ) + if not public_group: + public_group = result.security_agent.create_group( name = 'public' ) + # Store public group id + result.security_agent.set_public_group( public_group ) + log.debug( "Public Group identified as id = %s." % ( public_group.id ) ) return result def get_suite(): diff --git a/lib/galaxy/model/orm/__init__.py b/lib/galaxy/model/orm/__init__.py new file mode 100644 index 00000000000..433e1e26f45 --- /dev/null +++ b/lib/galaxy/model/orm/__init__.py @@ -0,0 +1,7 @@ +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) + +from sqlalchemy import * +from sqlalchemy.orm import * + +from sqlalchemy.ext.orderinglist import ordering_list diff --git a/lib/galaxy/model/orm/ext/__init__.py b/lib/galaxy/model/orm/ext/__init__.py new file mode 100644 index 00000000000..230b8aec908 --- /dev/null +++ b/lib/galaxy/model/orm/ext/__init__.py @@ -0,0 +1,3 @@ +""" +Galaxy specific SQLAlchemy extensions. +""" \ No newline at end of file diff --git a/lib/galaxy/model/orm/ext/assignmapper.py b/lib/galaxy/model/orm/ext/assignmapper.py new file mode 100644 index 00000000000..d2b7e2ca7b4 --- /dev/null +++ b/lib/galaxy/model/orm/ext/assignmapper.py @@ -0,0 +1,62 @@ +""" +This is similar to the assignmapper extensions in SQLAclhemy 0.3 and 0.4 but +with some compatibility fixes. It assumes that the session is a ScopedSession, +and thus has the "mapper" method to attach contextual mappers to a class. It +adds additional query and session methods to the class to support the +SQLAlchemy 0.3 style of access. The following methods which would normally be +accessed through "Object.query().method()" are available directly through the +object: + + 'get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances' + +Additionally, the following Session methods, which normally accept an instance +or list of instances, are available directly through the objects, e.g. +"Session.flush( [instance] )" can be performed as "instance.flush()": + + 'refresh', 'expire', 'delete', 'expunge', 'update' +""" + +__all__ = [ 'assign_mapper' ] + +from sqlalchemy import util, exceptions +import types +from sqlalchemy.orm import mapper, Query + +def _monkeypatch_query_method( name, session, class_ ): + def do(self, *args, **kwargs): + ## util.warn_deprecated('Query methods on the class are deprecated; use %s.query.%s instead' % (class_.__name__, name)) + return getattr( class_.query, name)(*args, **kwargs) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, classmethod(do)) + +def _monkeypatch_session_method(name, session, class_, make_list=False): + def do(self, *args, **kwargs): + if make_list: + self = [ self ] + return getattr(session, name)( self, *args, **kwargs ) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, do) + +def assign_mapper( session, class_, *args, **kwargs ): + m = class_.mapper = session.mapper( class_, *args, **kwargs ) + for name in ('get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances'): + _monkeypatch_query_method(name, session, class_) + for name in ('refresh', 'expire', 'delete', 'expunge', 'update'): + _monkeypatch_session_method(name, session, class_) + for name in ( 'flush', ): + _monkeypatch_session_method( name, session, class_, make_list=True ) + return m diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..4bd8cf2fe44 --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,290 @@ +""" +Galaxy Security + +""" +import logging +from galaxy.util.bunch import Bunch + +log = logging.getLogger(__name__) + +class RBACAgent: + """Class that handles galaxy security""" + permitted_actions = Bunch( + # The ability to edit the metadata of the associated dataset + DATASET_EDIT_METADATA = 'edit metadata', + # The ability to change the permissions of a dataset (so specifically, to add and modify + # group_dataset_association rows where the dataset is the dataset for which the permission is set). + DATASET_MANAGE_PERMISSIONS = 'manage permissions', + # The ability to perform any read only operation on the dataset (view, display at external site, + # use in a job, etc). + DATASET_ACCESS = 'access' + ) + permitted_action_descriptions = Bunch( + DATASET_EDIT_METADATA = "User can edit this dataset's metadata in the library", + DATASET_MANAGE_PERMISSIONS = "User can manage the groups and group permitted actions associated with this dataset", + DATASET_ACCESS = "User can import this dataset into their history for analysis" + ) + def allow_action( self, user, action, **kwd ): + raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + def guess_derived_permissions_for_datasets( self, datasets = [] ): + raise "Unimplemented Method" + def associate_components( self, **kwd ): + raise 'No valid method of associating provided components: %s' % kwd + def get_group( self, id ): + raise 'No valid method of retrieving group %s' % ( id ) + def create_group( self, **kwd ): + raise 'No valid method of creating group with %s' % ( kwd ) + def create_private_user_group( self, user ): + raise "Unimplemented Method" + def user_set_default_access( self, user, permissions = None, history = False, dataset = False ): + raise "Unimplemented Method" + def setup_new_user( self, user ): + self.user_set_default_access( user, history = True, dataset = True ) + self.associate_components( user=user, group=self.get_public_group() ) + def history_set_default_access( self, history, permissions=None, dataset=False ): + raise "Unimplemented Method" + def set_public_group( self, group ): + raise "Unimplemented Method" + def get_public_group( self ): + raise "Unimplemented Method" + def guess_public_group( self ): + raise "Unimplemented Method" + def set_dataset_permissions( self, dataset, permissions ): + raise "Unimplemented Method" + def set_dataset_permitted_actions( self, dataset ): + raise "Unimplemented Method" + def get_component_associations( self, **kwd ): + raise "Unimplemented Method" + def components_are_associated( self, **kwd ): + return bool( self.get_component_associations( **kwd ) ) + def convert_permitted_action_strings( self, permitted_action_strings ): + """ + When getting permitted actions from an untrusted source like a + form, ensure that they match our actual permitted actions. + """ + return filter( lambda x: x is not None, [ self.permitted_actions.get( action_string ) for action_string in permitted_action_strings ] ) + def get_permitted_action_description( self, permitted_action ): + """ + Return the description of a permitted_action, regardless of + whether permitted_action is a key or value. + """ + if self.permitted_action_descriptions.get( permitted_action ) is not None: + return self.permitted_action_descriptions.get( permitted_action ) + else: + for k, v in self.permitted_actions.items(): + if v == permitted_action: + return self.permitted_action_descriptions.get( k ) + return permitted_action # so at least something useful is printable + +class GalaxyRBACAgent( RBACAgent ): + def __init__( self, model, permitted_actions=None ): + self.model = model + if permitted_actions: + self.permitted_actions = permitted_actions + def allow_action( self, user, action, **kwd ): + if 'dataset' in kwd: + return self.allow_dataset_action( user, action, kwd['dataset'] ) + raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + def allow_dataset_action( self, user, action, dataset ): + """Returns true when user has permission to perform an action""" + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + if user is None: + try: + public_assoc = [ gda for gda in dataset.groups if gda.group == self.get_public_group() ][0] + except: + # There'll be an IndexError if the dataset doesn't have a gda with public + return False + if action in public_assoc.permitted_actions: + return True + elif user is not None: + # Loop through permitted_actions and if allowed return true: + # Check permitted_actions associated with dataset through groups + for group_dataset_assoc in dataset.groups: + if self.components_are_associated( user = user, group = group_dataset_assoc.group ): + if action in group_dataset_assoc.permitted_actions: + return True + return False # No user and dataset not in public group, or user lacks permission + def guess_derived_permissions_for_datasets( self, datasets=[] ): + """Returns a list of group/action tuples for the output dataset based upon provided datasets""" + intersect = None + priority_access_perms = None + for dataset in datasets: + # Determine access groups for output datasets - these groups are the + # intersection across all inputs. If we end up with no intersection + # between inputs, then we rely on priorities + if isinstance( dataset, self.model.HistoryDatasetAssociation ): + dataset = dataset.dataset + assocs = [ assoc for assoc in dataset.groups ] + for assoc in assocs: + if priority_access_perms is None or priority_access_perms[0].priority < assoc.group.priority: + priority_access_perms = ( assoc.group, assoc.permitted_actions ) + if intersect is None: + intersect = [ (a.group, a.permitted_actions) for a in assocs ] + else: + # Intersect existing perms with new perms + #access_assocs = filter( lambda x: x.group in [ a.group for a in access_assocs ], assocs ) + new_intersect = [] + for group, actions in intersect: + matches = filter( lambda x: x.group == group, assocs ) + # Could a dataset ever have more than one GDA with the same group id? + if len( matches ) > 1: + log.error( "Unable to derive permissions, duplicate group_dataset_association rows exist for group %d, dataset %d" % (group.id, dataset.id) ) + elif len( matches ) == 1: + # compare permitted_actions + pa_intersect = filter( lambda x: x in actions, matches[0].permitted_actions ) + new_intersect.append( ( group, pa_intersect ) ) + intersect = new_intersect + # If we have no groups left after intersection, take the highest priority group + if not intersect: + if priority_access_perms: + intersect = [ priority_access_perms ] + return intersect + def get_group( self, id ): + return self.model.Group.get( id ) + raise 'No valid method of retrieving requested group %s' % ( id ) + def create_group( self, **kwd ): + rval = self.model.Group( **kwd ) + rval.flush() + return rval + raise 'No valid method of creating group with %s' % ( kwd ) + def associate_components( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to associate.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.associate_group_dataset( kwd['group'], kwd['dataset'] ) + elif 'permissions' in kwd: + return self.associate_group_dataset( kwd['permissions'][0], kwd['dataset'], kwd['permissions'][1] ) + elif 'user' in kwd: + if 'group' in kwd: + return self.associate_user_group( kwd['user'], kwd['group'] ) + raise 'No valid method of associating provided components: %s' % kwd + def associate_group_dataset( self, group, dataset, permitted_actions=[ RBACAgent.permitted_actions.DATASET_ACCESS ] ): + # HACK: The default permitted_actions should really not be used... need to find cases where this is done and correct it + assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) + assoc.flush() + return assoc + def associate_user_group( self, user, group ): + assoc = self.model.UserGroupAssociation( user, group ) + assoc.flush() + return assoc + def create_private_user_group( self, user ): + # Create private group + group_name = "%s private group" % user.email + group = self.model.Group( name=group_name, priority=10 ) + group.flush() + # Add user to group + self.associate_components( group=group, user=user ) + group.flush() + return group + def user_set_default_access( self, user, permissions = None, history = False, dataset = False ): + if permissions is None: + permissions = [ ( self.create_private_user_group( user ), self.permitted_actions.__dict__.values() ) ] + if permissions is not None: + for assoc in user.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group, permitted_actions in permissions: + assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) + assoc.flush() + if history: + for history in user.histories: + self.history_set_default_access( history, permissions=permissions, dataset=dataset ) + def user_get_default_access( self, user ): + return [ ( duga.group, duga.permitted_actions ) for duga in user.default_groups ] + def history_set_default_access( self, history, permissions=None, dataset=False ): + if permissions is None: + if history.user: + permissions = self.user_get_default_access( history.user ) + else: + permissions = [ ( self.get_public_group(), self.permitted_actions.__dict__.values() ) ] + if permissions is not None: + for assoc in history.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group, permitted_actions in permissions: + assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) + assoc.flush() + if dataset: + for data in history.datasets: + for hda in data.dataset.history_associations: + if history.user and hda.history not in history.user.histories: + # This will occur when a user logs in and has datasets in their previously-public history. + self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) + break + else: + if self.allow_action( history.user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=data.dataset ): + self.set_dataset_permissions( data.dataset, permissions ) + def history_get_default_access( self, history ): + return [ ( dhga.group, dhga.permitted_actions ) for dhga in history.default_groups ] + def get_public_group( self ): + return self.model.Group.get_public_group() + def set_public_group( self, group ): + return self.model.Group.set_public_group( group ) + def guess_public_group( self ): + return self.model.Group.guess_public_group() + def set_dataset_permissions( self, dataset, permissions ): + """ + Apply permissions (a list of (group, permitted_action) tuples) + to a dataset, removing any existing permissions. permissions can + also be a list of GroupDatasetAssociations (for simplicity). + """ + if isinstance( dataset, self.model.HistoryDatasetAssociation ): + dataset = dataset.dataset + for group_dataset_assoc in dataset.groups: + group_dataset_assoc.delete() + group_dataset_assoc.flush() + if len( permissions ) and isinstance( permissions[0], self.model.GroupDatasetAssociation ): + permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] + for ptuple in permissions: + self.associate_components( dataset=dataset, permissions=ptuple ) + def get_dataset_permissions( self, dataset, group_id=None ): + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + if group_id is not None: + return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups if gda.group_id == int(group_id) ][0] + else: + return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups ] + def get_component_associations( self, **kwd ): + # TODO, Nate: Make sure this method is functionally correct. + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.model.GroupDatasetAssociation.get_by( group_id = kwd['group'].id, dataset_id = kwd['dataset'].id ) + elif 'user' in kwd: + if 'group' in kwd: + return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) + raise 'No valid method of associating provided components: %s' % kwd + def dataset_has_group( self, dataset_id, group_id ): + return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) + def check_folder_contents( self, user, entry ): + """ + Return true if there are any datasets under 'folder' that the + user has access permission on. We do this a lot and it's a + pretty inefficient method, optimizations are welcomed. + """ + if isinstance( entry, self.model.Library ): + return self.check_folder_contents( user, entry.root_folder ) + elif isinstance( entry, self.model.LibraryFolderDatasetAssociation ): + return self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=entry ) + elif isinstance( entry, self.model.LibraryFolder ): + for dataset in entry.active_datasets: + if self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=dataset ): + return True + for folder in entry.active_folders: + if self.check_folder_contents( user, folder ): + return True + return False + else: + raise 'Passed an illegal object to check_folder_contents: %s' % type( entry ) + + +def get_permitted_actions( self, filter=None ): + '''Utility method to return a subset of RBACAgent's permitted actions''' + if filter is None: + return RBACAgent.permitted_actions + if not filter.endswith('_'): + filter += '_' + tmp_bunch = Bunch() + [tmp_bunch.__dict__.__setitem__(k, v) for k, v in RBACAgent.permitted_actions.items() if k.startswith(filter)] + return tmp_bunch diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index c106d301e56..8cb7c044719 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1142,6 +1142,7 @@ class Tool: else: visible = False ext = fields.pop(0).lower() child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + self.app.security_agent.set_dataset_permissions( child_dataset.dataset, outdata.dataset.groups ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1178,6 +1179,7 @@ class Tool: ext = fields.pop(0).lower() # Create new primary dataset primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + self.app.security_agent.set_dataset_permissions( primary_data.dataset, outdata.dataset.groups ) primary_data.flush() # Move data from temp location to dataset location shutil.move( filename, primary_data.file_name ) diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index 44544ee4768..c3c21af95ea 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -45,6 +45,9 @@ class DefaultToolAction( object ): assoc.flush() data = new_data break + # TODO, Nate: Make sure the permitted actions here are appropriate. + if data and not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset=data ): + raise "User does not have permission to use a dataset (%s) provided for input." % data.id return data if isinstance( input, DataToolParameter ): if isinstance( value, list ): @@ -81,6 +84,14 @@ class DefaultToolAction( object ): data = NoneDataset( datatypes_registry = trans.app.datatypes_registry ) if data.dbkey not in [None, '?']: input_dbkey = data.dbkey + + # Determine output dataset permitted_actions list + existing_datasets = [ inp for inp in inp_data.values() if inp ] + if existing_datasets: + output_permissions = trans.app.security_agent.guess_derived_permissions_for_datasets( existing_datasets ) + else: + # No valid inputs, we will use history defaults + output_permissions = trans.app.security_agent.history_get_default_access( trans.history ) # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -119,6 +130,7 @@ class DefaultToolAction( object ): data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) # Commit the dataset immediately so it gets database assigned unique id data.flush() + trans.app.security_agent.set_dataset_permissions( data.dataset, output_permissions ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations @@ -178,6 +190,9 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: + # TODO, Nate: Make sure the permitted actions here are appropriate. + if not trans.app.security_agent.allow_action( trans.user, dataset.permitted_actions.DATASET_ACCESS, dataset=dataset ): + raise "User does not have permission to use a dataset (%s) provided for input." % data.id job.add_input_dataset( name, dataset ) else: job.add_input_dataset( name, None ) diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index b3904436a63..9fba4fe467d 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,8 +65,9 @@ class UploadToolAction( object ): return dict( output=data_list[0] ) def upload_empty(self, trans, err_code, err_msg): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) - data.name = err_code + data = trans.app.model.HistoryDatasetAssociation( create_dataset=True ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( trans.history ) ) + data.name = err_code data.extension = "txt" data.dbkey = "?" data.info = err_msg @@ -85,12 +86,12 @@ class UploadToolAction( object ): if not os.path.getsize( temp_name ) > 0: raise BadFileException( "you attempted to upload an empty file." ) - # See if we have a gzipped file, which, if it passes our restrictions, we'll decompress on the fly. + # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress on the fly. is_gzipped, is_valid = self.check_gzip( temp_name ) if is_gzipped and not is_valid: raise BadFileException( "you attempted to upload an inappropriate file." ) elif is_gzipped and is_valid: - #We need to decompress the temp_name file + # We need to uncompress the temp_name file CHUNK_SIZE = 2**20 # 1Mb fd, uncompressed = tempfile.mkstemp() gzipped_file = gzip.GzipFile( temp_name ) @@ -159,6 +160,7 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( trans.history ) ) data.name = file_name data.dbkey = dbkey data.info = info diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 3cc4e9826c4..6a2f278f47b 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -977,6 +977,8 @@ class DrillDownSelectToolParameter( ToolParameter ): class DataToolParameter( ToolParameter ): + # TODO, Nate: Make sure the following unit tests appropriately test the dataset security + # components. Add as many additional tests as necessary. """ Parameter that takes on one (or many) or a specific set of values. @@ -984,19 +986,35 @@ class DataToolParameter( ToolParameter ): displayed as radio buttons and multiple selects as a set of checkboxes >>> # Mock up a history (not connected to database) - >>> from galaxy.model import History, HistoryDatasetAssociation + >>> from galaxy.model import History, HistoryDatasetAssociation, User, Group >>> from galaxy.util.bunch import Bunch + >>> from galaxy.security import GalaxyRBACAgent + >>> import galaxy.model + >>> security_agent = GalaxyRBACAgent( galaxy.model ) >>> hist = History() >>> hist.flush() - >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) ) + >>> group = Group( 'test' ) + >>> group.flush() + >>> Group.public_id = group.id + >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) + >>> security_agent.set_dataset_permissions( dataset1, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) + >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) + >>> security_agent.set_dataset_permissions( dataset2, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) + >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) + >>> security_agent.set_dataset_permissions( dataset3, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) + >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) + >>> security_agent.set_dataset_permissions( dataset4, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) + >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) + >>> security_agent.set_dataset_permissions( dataset5, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) + >>> hist.add_dataset( dataset1 ) + >>> hist.add_dataset( dataset2 ) + >>> hist.add_dataset( dataset3 ) + >>> hist.add_dataset( dataset4 ) + >>> hist.add_dataset( dataset5 ) >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah - >>> print p.get_html( trans=Bunch( history=hist ) ) + >>> print p.get_html( trans=Bunch( history=hist, user=None, app=Bunch( security_agent = security_agent ) ) )   Priority: + + %if len( users ) == 0: + There are no Galaxy users + %else: + <% + render_quick_find = len( users ) > 50 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + Add Members to Group - Quick Find + + + |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z + + + %else: + Add Members to Group + %endif + + + %for user in users: + <% email = unescape( user[1], unentities ) %> + %if render_quick_find and not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + + + %if render_quick_find and email.upper().startswith( curr_anchor ): + %if not anchored: + +
+
quick find
+
+ <% anchored = True %> + %endif + ${email} + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: + +
+
quick find
+
+ <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${email} + %endif + + + %endfor + + + %endif +
+ + + + + + diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako new file mode 100644 index 00000000000..5afe2ae809f --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,73 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Permitted Actions on Datasets +
+
+ Libraries  |   + Groups  |   + Users +
+

Manage Permitted Actions on Datasets for Group '${group.name}'

+ + %if msg: + + %endif + + %if len( group.datasets ) == 0: + + %else: + + + + + <% ctr = 0 %> + + + %for gda in gdas: + <% permissions = trans.app.security_agent.get_dataset_permissions( gda, group.id ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + + <% ctr += 1 %> + %endfor + + + %endif +

${msg}

 
The group you selected has no associated datasets.
Association Names/InfoPermitted Actions
+ %if gda.dataset.library_associations: + Library name(s):
+ %endif +
    + %for da in gda.dataset.library_associations: +
  • ${da.name} (${da.info})
  • + %endfor +
+ %if gda.dataset.history_associations: + History name(s):
+ %endif +
    + %for da in gda.dataset.history_associations: +
  • ${da.name} (${da.info})
  • + %endfor +
+
+ %for pa in trans.app.model.Dataset.permitted_actions: + <% pa_val = trans.app.security_agent.permitted_actions.__dict__[pa] %> + + ${pa_val}
${trans.app.security_agent.get_permitted_action_description(pa)}
+
+ %endfor +
+
+
diff --git a/templates/admin/dataset_security/group_members.mako b/templates/admin/dataset_security/group_members.mako new file mode 100644 index 00000000000..9acd9faafe2 --- /dev/null +++ b/templates/admin/dataset_security/group_members.mako @@ -0,0 +1,54 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Group Members + +%if msg: +
${msg}
+%endif + +

Members of Group '${gn}'

+ +%if not deleted: + + +%endif + +%if len( members ) == 0: + + Group '${gn}' contains no members + +%else: + + + + <% ctr = 0 %> + %for member in members: + <% email = unescape( member[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + <% ctr += 1 %> + %endfor +
Username
${email}
+ +%endif diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako new file mode 100644 index 00000000000..03ce26ded29 --- /dev/null +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -0,0 +1,112 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Manage Group Membership + +%if msg: +
${msg}
+%endif + +<% gn = unescape( group_name, unentities ) %> + +

Members of group '${gn}'

+ + + + + + +
 
+ + + %if len( users ) == 0: + + %else: + + + + + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + + %endif + + +
There are no Galaxy users
+ |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z +
+ <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% + email = unescape( user[1], unentities ) + check = False + %> + %for member in members: + <% member_email = unescape( member[1], unentities ) %> + %if email == member_email: + <% + check = True + break + %> + %endif + %endfor + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: +
+ %if email.upper().startswith( curr_anchor ): + %if not anchored: +

+ <% anchored = True %> + %endif + %if check: + ${email} + %else: + ${email} + %endif + %else: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: +

+ <% + curr_anchor = anchor + anchored = True + %> + %endif + %if check: + ${email} + %else: + ${email} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
+
+ diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako new file mode 100644 index 00000000000..63a3920684a --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,137 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +## Render a row +<%def name="render_row( group_name, group )"> + ${group_name} + ${group[2]} + ${group[3]} + %if group[4] > 0: + ${group[4]} + %else: + ${group[4]} + %endif + + %if len( group[5] ) == 1: + ${group[5][0]} + %elif len( group[5] ) > 1: + %for da in group[5]: + ${da}
+ %endfor + %endif + + Mark group deleted + + +<%def name="title()">Groups + +%if msg: +
${msg}
+%endif + +

Groups

+ + + +%if len( groups ) == 0: + There are no Galaxy groups +%else: + + + <% + render_quick_find = len( groups ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + + + + %for group in groups: + <% group_name = unescape( group[1], unentities ) %> + %if render_quick_find and not group_name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + + %if ctr % 2 == 1: +
+ %endif + %if render_quick_find and group_name.upper().startswith( curr_anchor ): + %if not anchored: +
+ + + %endif + ${render_row( group_name, group )} + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if group_name.upper().startswith( anchor ): + %if not anchored: + + + + %endif + ${render_row( group_name, group )} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( group_name, group )} + %endif + %if ctr % 2 == 1: + + %endif + <% ctr += 1 %> + + %endfor + %endif +
+ Jump to letter: + %for a in anchors: + | ${a} + %endfor +
NamePriorityMembersDatasetsGroup Permitted Actions on Datasets 
+ + + <% anchored = True %> +
+ + + <% + curr_anchor = anchor + anchored = True + %> +
+ diff --git a/templates/admin/dataset_security/index.mako b/templates/admin/dataset_security/index.mako new file mode 100644 index 00000000000..1a013ec1fc6 --- /dev/null +++ b/templates/admin/dataset_security/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Dataset Security +
+
Dataset Security
+ + %if msg: + + %endif + + +

${msg}

Groups
Users
+
diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako new file mode 100644 index 00000000000..546842b59f4 --- /dev/null +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -0,0 +1,72 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% email = unescape( user_email, unentities ) %> + +<%def name="title()">Group Membership + +%if msg: +
${msg}
+%endif + +

Groups of which '${email}' is a member

+ +%if len( groups ) == 0: + + User '${email}' belongs to no groups + +%else: + + + + + + + + + + + <% ctr = 0 %> + %for group in groups: + <% + gn = unescape( group[1], unentities ) + library_ids = '' + for library_id in group[5]: + library_ids += "%s," % library_id + library_ids = library_ids.rstrip( ',' ) + %> + %if ctr % 2 == 1: + + %else: + + %endif + + + %if group[3] > 0: + + %else: + + %endif + + + + <% ctr += 1 %> + %endfor + +
GroupPriorityDatasetsPermitted Actions on DatasetsContaining Libraries
${gn}${group[2]}${group[3]}${group[3]} + %for da in group[4]: + ${da}
+ %endfor +
+ %if len( group[5] ) > 0: + ${len( group[5] )} + %else: + ${len( group[5] )} + %endif +
+ +%endif diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..12e1b4ca550 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,85 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Users + +%if msg: +
${msg}
+%endif + +

Users

+ +%if len( users ) == 0: + There are no Galaxy users +%else: + + <% + render_quick_find = len( users ) > 50 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + %for ctr, user in enumerate( users ): + <% email = unescape( user[1], unentities ) %> + %if render_quick_find and not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + + + + %endfor +
+ Jump to letter: + %for a in anchors: + | ${a} + %endfor +
Email address
+ %if render_quick_find and email.upper().startswith( curr_anchor ): + %if not anchored: + + + <% anchored = True %> + %endif + ${email} + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: + + + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${email} + %endif +
+ +%endif + diff --git a/templates/admin/index.mako b/templates/admin/index.mako new file mode 100644 index 00000000000..e3d642f55b3 --- /dev/null +++ b/templates/admin/index.mako @@ -0,0 +1,112 @@ +<%inherit file="/base_panels.mako"/> + +<%def name="init()"> +<% + self.has_left_panel=True + self.has_right_panel=False + self.active_view="admin" +%> + + +<%def name="stylesheets()"> + + ${parent.stylesheets()} + + ## TODO: Clean up these styles and move into panel_layout.css (they are + ## used here and in the editor). + + + + +<%def name="left_panel()"> +
+
Administration
+
+
+
+
+
+ Security +
+ +
+
+ Data +
+ +
+
+ Tools +
+ +
+
+
+ + +<%def name="center_panel()"> + + + + diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako new file mode 100644 index 00000000000..31037536dbe --- /dev/null +++ b/templates/admin/library/browser.mako @@ -0,0 +1,179 @@ +<%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + ##if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + ## return "" + pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True + %> +
  • +
    + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif + +
    +
    + Add new dataset to this folder + Create a new subfolder in this folder + Rename this folder + %if subfolder: + Remove this folder and its contents + %endif +
    +
  • + %if subfolder: +