diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 0c4ea728395..1964945012e 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""" @@ -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 432499bef7a..ac54a889b7a 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 ) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 90d44ab47f4..7c58d38f632 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -110,7 +110,7 @@ class Gmaj( data.Data ): "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 } ) + "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, 'copy_access_from': dataset.id } ) } class_name = "edu.psu.bx.gmaj.MajApplet.class" archive = "/static/gmaj/gmaj.jar" @@ -180,7 +180,7 @@ class Laj( data.Text ): "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 } ), + "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ), "noseq": "true" } class_name = "edu.psu.cse.bio.laj.LajApplet.class" diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 7d08ccb2e46..4ef5a34744e 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,252 +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.dbkey = dbkey - self.designation = designation - self._metadata = metadata or dict() - 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 - - @property - def ext( self ): - return self.extension - - @property - def states( self ): - return self.dataset.states - - 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 ) + 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 ) - def get_metadata( self ): - if not self._metadata: - self._metadata = dict() - return MetadataCollection( self, self.datatype.metadata_spec ) - 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 ) +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 ) - """ - 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() - 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 ): - #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 ) - 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 copy( self, copy_children = False, parent_id = 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 ) - des.flush() - if copy_children: - for child in self.children: - child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs - des.flush() - return des - - 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 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 +class UserGroupAssociation( object ): + def __init__( self, user, group ): 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 + self.group = group - 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 ) +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 - 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): - des = History() - des.flush() - des.name = self.name - des.user_id = self.user_id - for data in self.datasets: - new_data = data.copy( copy_children = True ) - des.add_dataset( new_data ) - new_data.flush() - des.hid_counter = self.hid_counter - des.flush() - return des - -# 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 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 class Dataset( object ): states = Bunch( NEW = 'new', @@ -356,6 +175,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 ): @@ -440,8 +260,347 @@ 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 self._metadata: + self._metadata = dict() + return MetadataCollection( self, self.datatype.metadata_spec ) + 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() + 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 ): + 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 ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + 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 ) + des.add_dataset( new_data ) + 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 + +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 ): + 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 ) + des.flush() + for child in self.children: + child_copy = child.to_history_dataset_association( parent_id = des.id ) + 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 ): + 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 ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + 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 ): @@ -483,7 +642,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 diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cdc8419c439..cd4aa9c3ee9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -19,6 +19,7 @@ from sqlalchemy import * from galaxy.model 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 ) @@ -65,7 +66,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 +73,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,6 +115,107 @@ 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 ), + 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 ), + 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( "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 ), @@ -255,6 +357,10 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab 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] ) ), + 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] ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), @@ -268,7 +374,10 @@ 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 +407,74 @@ 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 = "users" ) ) ) + +assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "groups" ), + group=relation( Group, backref = "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] ) ), + 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 ) ) ) @@ -411,6 +588,41 @@ def init( file_path, url, engine_options={}, create_tables=False ): result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) result.context = context result.create_tables = create_tables + #load local galaxy security policy + result.security_agent = GalaxyRBACAgent( result ) + # TODO, Nate: The following may not work for our Galaxy instances because there are too + # many rows that need updating ( I think ) even though we have eliminated all of the + # Role stuff. Maybe we can test this to see how long it takes for about 1000 datasets. + # If we decide to use this approach rather than SQL commands to populate the tables, + # then this needs to be thoroughly tested to ensure the data is populated as expected + # (i.e., make sure naything that is public gets the public security settings, etc). + # + # Set up default table entries here, only exist for group access because + # permitted actions are exclusively restricted to the association between a group + # and a dataset + if result.Group.count() == 0: + log.warning( "There were no groups located, setting up default (public) group." ) + # Create public group + public_group = result.security_agent.create_group( name='public' ) + # Store public group id + result.security_agent.set_public_group( public_group ) + # Loop through all histories and set up rbac on users, histories and datasets + for history in result.History.select( result.History.table.c.purged == False ): + if history.user: + if not history.user.default_groups: + result.security_agent.setup_new_user( history.user ) + history.user.flush() + else: + result.security_agent.history_set_default_access( history, dataset=True ) + history.flush() + # Add all datasets which aren't in a history to the public group + orphans = result.Dataset.get_by( history_id = None ) + if orphans: + for dataset in orphans: + result.security_agent.set_dataset_groups( dataset, [ public_group ] ) + else: + result.security_agent.guess_public_group() + log.debug( "Public Group identified as id = %s." % ( Group.public_id ) ) return result def get_suite(): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..0eaba298d10 --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,245 @@ +""" +Galaxy Security + +""" +import logging +from galaxy.util.bunch import Bunch + +log = logging.getLogger(__name__) + +# TODO, Nate: Think about whether the following permitted actions are appropriate for the dataset and +# group objects. What should be the default "public" permitted actions? Make sure that the public group +# and public datasets are set with the correct permitted actions. Also make sure that "private" settings +# are correct when an authenticated user creates things inside their "private" environment. +class RBACAgent: + """Class that handles galaxy security""" + permitted_actions = Bunch( + # The ability to edit the metadata of the associated dataset + DATASET_EDIT_METADATA = 'dataset_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 = 'dataset_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 = 'dataset_access' + ) + 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_groups_permitted_actions_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, groups = 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, groups=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_groups( self, dataset, groups ): + 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 ) ) + +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 dataset is in public group, we always return true for viewing and using + # This may need to change when the ability to alter groups and permitted_actions is allowed + if action == self.permitted_actions.DATASET_ACCESS and \ + self.components_are_associated( group = self.get_public_group(), dataset = dataset ): + 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_groups_for_datasets( self, datasets=[] ): + # TODO, Nate: Make sure this method is functionally correct. + """Returns a list of groups for the output dataset based upon itself and provided datasets""" + access_groups = None + priority_access_group = 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 + groups = [ data_group_assoc.group for data_group_assoc in dataset.groups ] + for group in groups: + if priority_access_group is None or priority_access_group.priority < group.priority: + priority_access_group = group + if access_groups is None: + access_groups = set( groups ) + else: + access_groups.intersection_update( set( groups ) ) + # Complete lists for output dataset access + if access_groups: + access_groups = list( access_groups) + else: + access_groups = [] + # If we have no groups left after intersection, take the highest priority group + if not access_groups: + if priority_access_group: + access_groups = [ priority_access_group ] + return access_groups + 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 '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 disassociate_components( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to disassociate.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.disassociate_group_dataset( kwd['group'], kwd['dataset'] ) + raise 'No valid method of associating provided components: %s' % kwd + def associate_group_dataset( self, group, dataset, permitted_actions=[] ): + if not permitted_actions: + if isinstance( dataset.permitted_actions, Bunch ): + permitted_actions = dataset.permitted_actions.__dict__.values() + else: + permitted_actions = dataset.permitted_actions + assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) + assoc.flush() + return assoc + def disassociate_group_dataset( self, group, dataset ): + assoc = self.model.GroupDatasetAssociation.selectone_by( group_id = group.id, dataset_id = dataset.id ) + assoc.delete() + assoc.flush() + 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, groups = None, history = False, dataset = False ): + # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. + if groups is None: + groups = [ self.create_private_user_group( user ) ] + if groups is not None: + for assoc in user.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + if isinstance( group, self.model.Group ): + permitted_actions = group.permitted_actions.__dict__.values() + else: + permitted_actions = group.permitted_actions + assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) + assoc.flush() + if history: + for history in user.histories: + self.history_set_default_access( history, groups=groups, dataset=dataset ) + def history_set_default_access( self, history, groups=None, dataset=False ): + # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. + if groups is None: + if history.user: + groups = [ assoc.group for assoc in history.user.default_groups ] + else: + groups = [ self.get_public_group() ] + if groups is not None: + for assoc in history.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + if isinstance( group, self.model.Group ): + permitted_actions = group.permitted_actions.__dict__.values() + else: + permitted_actions = group.permitted_actions + 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: + self.set_dataset_groups( data.dataset, [ self.get_public_group() ] ) + break + else: + self.set_dataset_groups( data.dataset, 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_groups( self, dataset, groups ): + if isinstance( dataset, self.model.HistoryDatasetAssociation ): + dataset = dataset.dataset + for group_dataset_assoc in dataset.groups: + group_dataset_assoc.delete() + group_dataset_assoc.flush() + for group in groups: + if not isinstance( group, self.model.Group ): + group = group.group + self.associate_components( dataset=dataset, group=group ) + 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 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 a56e6a829b4..7b5c4b0d305 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,6 +1085,8 @@ 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 ) + # TODO, Nate: Make sure the following is functionally correct. + self.app.security_agent.set_dataset_groups( child_dataset.dataset, outdata.dataset.groups ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1121,6 +1123,8 @@ 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 ) + # TODO, Nate: Make sure the following is functionally correct. + self.app.security_agent.set_dataset_groups( 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 f2bb0cd8185..bd74d188ee6 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -43,6 +43,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 ): @@ -79,6 +82,15 @@ 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_access_groups = trans.app.security_agent.guess_derived_groups_for_datasets( existing_datasets ) + else: + # No valid inputs, we will use history defaults + output_access_groups = [ group.group for group in trans.history.default_groups ] + # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -120,6 +132,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_groups( data.dataset, output_access_groups ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations @@ -183,6 +196,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..a9623ae40e0 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,8 +65,10 @@ 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 ) + # TODO, Nate: Make sure the following is appropriate. + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + data.name = err_code data.extension = "txt" data.dbkey = "?" data.info = err_msg @@ -85,12 +87,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 +161,8 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + # TODO, Nate: Make sure the following is appropriate. + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) 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 b10b51bdb5e..b626e665472 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -972,6 +972,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. @@ -979,19 +981,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_groups( dataset1, [ group ] ) + >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset2, [ group ] ) + >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset3, [ group ] ) + >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset4, [ group ] ) + >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset5, [ group ] ) + >>> 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: + 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 + + + + + <% + 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 ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: + + %else: + + %endif + + %if email.upper().startswith( curr_anchor ): + %if not anchored: +

+ <% anchored = True %> + %endif + ${email} + %else: + %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 + %endif + + <% ctr += 1 %> + %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..58406d7edbf --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,71 @@ +<%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()">Permitted Actions on Datasets +
+
+ Libraries  |   + Groups  |   + Users +
+

Manage Permitted Actions on Datasets for Group '${gn}'

+ + %if msg: + + %endif + + %if len( gdas ) == 0: + + %else: + + + + + + <% ctr = 0 %> + + %for gda in gdas: + %if ctr % 2 == 1: + + %else: + + %endif + + + + + <% ctr += 1 %> + %endfor + + + %endif +

${msg}

 
There is no Galaxy group named '${gn}'
GroupPriorityPermitted Actions on Datasets
${gn}${gda[0]} + %for da in dataset_actions: + <% check = False %> + %for action in gda[1]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
+ %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..36e407959f6 --- /dev/null +++ b/templates/admin/dataset_security/group_members.mako @@ -0,0 +1,47 @@ +<%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()">Create Group +
+
+ Libraries  |   + Groups  |   + Users +
+
+ Manage group membership +
+
+
Members of Group '${gn}'
+ + %if msg: + + %endif + + %if len( members ) == 0: + + %else: + <% ctr = 0 %> + %for member in members: + <% email = unescape( member[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + <% ctr += 1 %> + %endfor + %endif +

${msg}

 
Group '${gn}' contains no members
${email}
+
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..c7defaa7697 --- /dev/null +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -0,0 +1,114 @@ +<%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 +
+
+ Libraries  |   + Groups  |   + Users +
+ + %if msg: + + %endif + <% gn = unescape( group_name, unentities ) %> + + + + +

${msg}

 
+ + + %if len( users ) == 0: + + %else: + + + + + + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + + %endif + + +
There are no Galaxy users
Members of '${gn}' - 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 +
+ <% + 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..0687802932e --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,67 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Groups +
+
+ Libraries  |   + Users +
+
+ Create a new group +
+ Manage deleted groups +
+
+

Groups

+ + %if msg: + + %endif + %if len( groups ) == 0: + + %else: + + + + + + + + + <% ctr = 0 %> + %for group in groups: + <% group_name = unescape( group[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + + %if group[4] > 0: + + %else: + + %endif + + + + <% ctr += 1 %> + %endfor + %endif +

${msg}

There are no Galaxy groups
GroupPriorityMembersDatasetsGroup Permitted Actions on Datasets 
${group_name}${group[2]}${group[3]}${group[4]}${group[4]} + %if len( group[5] ) == 1: + ${group[5][0]} + %elif len( group[5] ) > 1: + %for da in group[5]: + ${da}
+ %endfor + %endif +
Mark group deleted
+
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..8a392a67e31 --- /dev/null +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -0,0 +1,56 @@ +<%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()">Create Group +
+
+ Libraries  |   + Groups  |   + Users +
+

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

+ + %if msg: + + %endif + %if len( groups ) == 0: + + %else: + + + + + + + <% ctr = 0 %> + %for group in groups: + <% gn = unescape( group[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + %if group[3] > 0: + + %else: + + %endif + + + <% ctr += 1 %> + %endfor + %endif +

${msg}

User '${email}' belongs to no groups
GroupPriorityDatasetsPermitted Actions on Datasets
${gn}${group[2]}${group[3]}${group[3]} + %for da in group[4]: + ${da}
+ %endfor +
+
diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..22abae62d54 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,80 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Users +
+
+ Groups  |   + Libraries +
+ + %if msg: + + %endif + + %if len( users ) == 0: + + %else: + + + + + <% + 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 ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + %endif +

${msg}

 
There are no Galaxy users
Galaxy Users - 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 +
+ %if email.upper().startswith( curr_anchor ): + %if not anchored: +

+ <% anchored = True %> + %endif + ${email} + %else: + %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 + %endif +
+
diff --git a/templates/admin/index.mako b/templates/admin/index.mako new file mode 100644 index 00000000000..84e90c84b55 --- /dev/null +++ b/templates/admin/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +
+

Galaxy Administration

+ + %if msg: + + %endif + + + +

${msg}

Dataset Security
Libraries
Reload a tool while the Galaxy server is running
+
diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako new file mode 100644 index 00000000000..a7877e54479 --- /dev/null +++ b/templates/admin/library/dataset.mako @@ -0,0 +1,123 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Edit Dataset Attributes + +<%def name="datatype( dataset, datatypes )"> + + + +<%def name="group_dataset_permitted_actions( dataset_actions, gda )"> + %for da in dataset_actions: + <% check = False %> + %for action in gda[2]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
+ %endfor + + +
+
Group Associations
+
+
+ + %for gda in gdas: +
${gda[1]}
+
+
+ ${group_dataset_permitted_actions( dataset_actions, gda )} +
+ %endfor +
+
+
+
+
+
Edit Attributes
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ %for element in metadata: +
+ +
+ ${element.get_html()} +
+
+
+ %endfor +
+ +
+
+
+ +
+ +
+
+ This will inspect the dataset and attempt to correct the above column values + if they are not accurate. +
+
+
+
+

+

+
Change data type
+
+
+ +
+ +
+ ${datatype( dataset, datatypes )} +
+
+ This will change the datatype of the existing dataset + but not modify its contents. Use this if Galaxy + has incorrectly guessed the type of your dataset. +
+
+
+
+ +
+
+
+
+manage containing folder +

diff --git a/templates/admin/library/folder.mako b/templates/admin/library/folder.mako new file mode 100644 index 00000000000..e93b64e7989 --- /dev/null +++ b/templates/admin/library/folder.mako @@ -0,0 +1,158 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + return render_dataset( component ) + %> + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> +

+
${data.display_name()}
+
+
+ ## Header row for history items (name, state, action buttons) +
+ %if data_state != 'ok': +
+ %endif +
+
+ edit attributes +
+ ##${data.display_name()} +
+ ## Body for history items, extra info and actions, data "peek" +
+ %if data_state == "queued": +
Job is waiting to run
+ %elif data_state == "running": +
Job is currently running
+ %elif data_state == "error": +
+ An error occurred running this job: ${data.display_info().strip()}, + report this error +
+ %elif data_state == "empty": +
No data: ${data.display_info()}
+ %elif data_state == "ok": +
+ ${data.blurb}, + format: ${data.ext}, + database: + %if data.dbkey == '?': + ${data.dbkey} + %else: + ${data.dbkey} + %endif +
+
Info: ${data.display_info()}
+ %if data.peek != "no peek": +
${data.display_peek()}
+ %endif + %else: +
Error: unknown dataset state "${data_state}".
+ %endif + ## Recurse for child datasets +
+
+
+ +## Render a folder +<%def name="render_folder( this_folder )"> +
+
Contents of Folder: ${this_folder.name}
+
+
+ <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> + %for component in components: + ${render_component( component )} + %endfor +
+
+ +
+
+
+ +<%def name="title()">Manage Folder: ${folder.name} +
+
+ Libraries  |   + Groups  |   + Users +
+
Change Folder Attributes
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
Manage Folder Contents: ${folder.name}
+
+
+ %if folder.parent: + Up a Level + %elif folder.library_root: + Manage Library + %endif +
+
+
+ ${render_folder( folder )} +
+
+
+
diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako new file mode 100644 index 00000000000..7e44442e1aa --- /dev/null +++ b/templates/admin/library/libraries.mako @@ -0,0 +1,19 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Libraries +
+
+ Groups  |   + Users +
+
Create a new library
+
+
Galaxy Libraries
+
+ %for library in libraries: + + %endfor +
+
diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako new file mode 100644 index 00000000000..5d979af8abb --- /dev/null +++ b/templates/admin/library/library.mako @@ -0,0 +1,35 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Library +
+
+ Libraries  |   + Groups  |   + Users +
+
Manage Library '${library.name}'
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ + +
 
+
+
+
+ Manage Root Folder +
diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako new file mode 100644 index 00000000000..3c641288b74 --- /dev/null +++ b/templates/admin/library/new_dataset.mako @@ -0,0 +1,82 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Create New Library Dataset +
+
+ Libraries  |   + Groups  |   + Users +
+
Create a new Library Dataset
+
+
+ +
+ +
+
+
+
+ +
+
+ Here you may specify a list of URLs (one per line) or paste the contents of a file. +
+
+
+
+ +
Yes
+
+ Use this option if you are entering intervals by hand. +
+
+
+
+ +
+ +
+
+ Which format? See help below +
+
+
+
+ +
+ +
+
+
+
+ + Multi-select list - hold the appropriate key while clicking to select multiple columns +
+ +
+
+
+
+ +
+
+
+
diff --git a/templates/admin/reload_tool.mako b/templates/admin/reload_tool.mako new file mode 100644 index 00000000000..1d050ea4764 --- /dev/null +++ b/templates/admin/reload_tool.mako @@ -0,0 +1,28 @@ +<%inherit file="/base.mako"/> + +
+

Reload a Tool

+ + %if msg: + + %endif + + + +

${msg}

+
+

+ Reload tool: + + +

+
+
+
diff --git a/templates/admin_main.mako b/templates/admin_main.mako deleted file mode 100644 index f9c26121124..00000000000 --- a/templates/admin_main.mako +++ /dev/null @@ -1,33 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Galaxy Administration - - - - - - - - -
-

Galaxy Administration

- %if msg: -

${msg}

- %endif -
-
-

Admin password:

-

- Reload tool: - - -

-
-
- diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index faf11771489..c54f6eb0d1a 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -1,5 +1,5 @@ <%inherit file="/base.mako"/> -<%def name="title()">Your saved histories +<%def name="title()">Edit Dataset Attributes <%def name="datatype( dataset, datatypes )"> @@ -130,3 +130,36 @@
+ +

+ +%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ): +

+
Change Permitted Actions
+
+
+ +
+ + <% checked = "" %> + %if not trans.app.security_agent.dataset_has_group( data.id, trans.app.model.Group.get_public_group().id ): + <% checked = " checked" %> + %endif +
+ +
+
+
+ This will prevent other users from viewing or utilizing this dataset, even if you share your history with them. +
+
+
+
+ +
+
+
+
+%endif diff --git a/templates/form.mako b/templates/form.mako index c003de02585..42fdb745ba3 100644 --- a/templates/form.mako +++ b/templates/form.mako @@ -21,9 +21,11 @@ $(function(){ cls += " form-row-error" %>
+ %if input.use_label: + %endif
diff --git a/templates/history/options.mako b/templates/history/options.mako index 4faa967742d..4bebc932dca 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,6 +18,7 @@ %endif %if app.config.enable_beta_features:
  • Construct workflow from the current history
  • +
  • Change default permitted actions for the current history
  • %endif
  • Share current history
  • %endif diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako new file mode 100644 index 00000000000..6607f002c41 --- /dev/null +++ b/templates/history/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permitted Actions + +%if trans.user: +
    +
    Change Default History Permitted Actions
    +
    +
    +
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% cur_groups = [ assoc.group for assoc in trans.get_history().default_groups ] %> +
    + + + %for group in user_groups: + + %endfor +
    GroupInOut
    ${group.name}
    +
    + +
    + +
    + This will change the default permitted actions assigned to new datasets for your current history. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif \ No newline at end of file diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako new file mode 100644 index 00000000000..1176bd2162a --- /dev/null +++ b/templates/library/libraries.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Libraries You Can Access +
    +
    Libraries You Can Access
    +
    + %for library in libraries: + + %endfor +
    +
    diff --git a/templates/library/library.mako b/templates/library/library.mako new file mode 100644 index 00000000000..373ff9c4118 --- /dev/null +++ b/templates/library/library.mako @@ -0,0 +1,69 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + render = False + # Check the folder's datasets to see what can be rendered + for library_folder_dataset_assoc in component.datasets: + if render: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + # TODO: Do we need to upgrade sqlalchemy? The following shouldn't be necessary if the mappers work correctly. + # Check the folder's sub-folders to see what can be rendered + for library_folder in component.folders: + render_component( library_folder ) + if render: + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + render = False + dataset = trans.app.model.Dataset.get( component.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + if render: + return render_dataset( component ) + %> + + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> +
    + ${data.name} +
    + + +## Render a folder +<%def name="render_folder( this_folder )"> +
    + Folder: ${this_folder.name} + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> +
    + %for component in components: + ${render_component( component )} + %endfor +
    +
    + + +<%def name="title()">View Library: ${library.name} +
    +
    Import from Library: ${library.name}
    +
    +
    + ${render_folder( library.root_folder )} +
    + +
    +
    +
    diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 877705614ed..cd3225e7994 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -32,7 +32,9 @@ ## Body for history items, extra info and actions, data "peek"
    - %if data_state == "queued": + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
    You do not have permission to view this dataset.
    + %elif data_state == "queued":
    Job is waiting to run
    %elif data_state == "running":
    Job is currently running
    @@ -100,4 +102,4 @@
    - \ No newline at end of file + diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 58d8f1776db..097d7707acc 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -20,6 +20,9 @@ | wiki | screencasts | blog + %if admin_user == "true": + | admin + %endif     diff --git a/templates/user/index.mako b/templates/user/index.mako index 2b11262cece..1047466aad0 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -8,6 +8,9 @@ %else: diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako new file mode 100644 index 00000000000..7b6b90f976d --- /dev/null +++ b/templates/user/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permitted Actions + +%if trans.user: +
    +
    Change Default Permitted Actions for new Histories
    +
    +
    +
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% cur_groups = [ assoc.group for assoc in trans.user.default_groups ] %> +
    + + + %for group in user_groups: + + %endfor +
    GroupInOut
    ${group.name}
    +
    + +
    + +
    + This will change the default permitted actions assigned to new datasets for new histories. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif \ No newline at end of file diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index e46f0c54ad1..69ccfe583ee 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -12,6 +12,7 @@ +
    diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml new file mode 100644 index 00000000000..e6dabfbf1b1 --- /dev/null +++ b/tools/data_source/access_libraries.xml @@ -0,0 +1,7 @@ + + + stored locally + + + + \ No newline at end of file diff --git a/tools/data_source/encode_import_code.py b/tools/data_source/encode_import_code.py index 09a6e8a9823..ddfbb0241e0 100644 --- a/tools/data_source/encode_import_code.py +++ b/tools/data_source/encode_import_code.py @@ -5,7 +5,8 @@ from shutil import copyfile #post processing, set build for data and add additional data to history def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr): - history = out_data.items()[0][1].history + base_dataset = out_data.items()[0][1] + history = base_dataset.history if history == None: print "unknown history!" return @@ -37,6 +38,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + description + ")" history.add_dataset( newdata ) + #TODO, Nate: Make sure the following is functionally correct + app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) app.model.flush() try: copyfile(filepath,newdata.file_name) diff --git a/tools/data_source/microbial_import_code.py b/tools/data_source/microbial_import_code.py index b6bc2f6bd85..e8816f093ff 100644 --- a/tools/data_source/microbial_import_code.py +++ b/tools/data_source/microbial_import_code.py @@ -84,7 +84,8 @@ from galaxy import datatypes, config, jobs from shutil import copyfile def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr): - history = out_data.items()[0][1].history + base_dataset = out_data.items()[0][1] + history = base_dataset.history if history == None: print "unknown history!" return @@ -128,6 +129,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + microbe_info[kingdom][org]['chrs'][chr]['data'][description]['feature'] +" for "+microbe_info[kingdom][org]['name']+":"+chr + ")" newdata.flush() + #TODO, Nate: Make sure the following is functionally correct + app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) history.add_dataset( newdata ) app.model.flush() try: diff --git a/tools/maf/maf_to_bed_code.py b/tools/maf/maf_to_bed_code.py index c8f1e905e8e..428486b3e37 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -27,12 +27,13 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr fields = line.split("\t") dbkey = fields[1] filepath = fields[2] - newdata = app.model.HistoryDatasetAssociation( create_dataset = True ) newdata.extension = "bed" newdata.name = basic_name + " (" + dbkey + ")" newdata.flush() history.add_dataset( newdata ) + #TODO, Nate: Make sure the following is functionally correct + app.security_agent.set_dataset_groups( newdata.dataset, output_data.dataset.groups ) newdata.flush() history.flush() app.model.flush() diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 6dcdf061f94..58ca578eb15 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -77,8 +77,8 @@ use_lint = false # NEVER enable this on a public site (even test or QA) use_interactive = true -# Admin Password -admin_pass = galaxy +# Admin Users - this should be a comma-separated list of valid Galaxy users +#admin_users = user1@bx.psu.edu,user2@bx.psu.edu # path to sendmail sendmail_path = /usr/sbin/sendmail