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 69d9dffe404..d9ed17b1aa5 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.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 - - @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,351 @@ 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 + + @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 ): + 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 +646,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..20c9f567bbf 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,109 @@ 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, unique=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 ), @@ -255,6 +359,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 +376,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 +409,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 = "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] ) ), + 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 ) ) ) @@ -411,6 +600,15 @@ 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 ) + # 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/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..82caa932bff --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,259 @@ +""" +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 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_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: + # When would this occur? + 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 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_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..4030cadd47e 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,6 +1085,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() @@ -1121,6 +1122,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 f2bb0cd8185..efc0ce99893 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,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] @@ -120,6 +131,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 @@ -183,6 +195,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 b10b51bdb5e..4167629927b 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_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..6648a72446f --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,72 @@ +<%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[0]: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da[0]}
${da[1]}
+
+ %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..79c44284459 --- /dev/null +++ b/templates/admin/dataset_security/group_members.mako @@ -0,0 +1,53 @@ +<%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  |   + %if deleted: + Deleted Groups  |   + %else: + Groups  |   + %endif + Users +
+ %if not deleted: + + %endif +
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..48cc138474a --- /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..bfe888535a2 --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,136 @@ +<%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 +
+
+ Libraries  |   + Users +
+ + + %if msg: + + %endif + %if len( groups ) == 0: + + %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' + %> + + + + + %else: + + %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 +

${msg}

There are no Galaxy groups
Galaxy Groups - 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 +
+
Galaxy Groups
GroupPriorityMembersDatasetsGroup 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..e5678635c96 --- /dev/null +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -0,0 +1,70 @@ +<%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 ) + 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 + %endif +

${msg}

User '${email}' belongs to no groups
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 +
+
diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..5c8be532e54 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,97 @@ +<%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: + <% + render_quick_find = len( users ) > 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' + %> + + + + + %else: + + %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 ctr % 2 == 1: +
+ %endif +
+ %if ctr % 2 == 1: + + %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 +
Galaxy Users
+ %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 +
+
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..aa73d81ee49 --- /dev/null +++ b/templates/admin/library/folder.mako @@ -0,0 +1,170 @@ +<%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 folder 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 folder items (name, state, action buttons) +
+ %if data_state != 'ok': +
+ %endif +
+
+ edit attributes + +
+ ##${data.display_name()} +
+ ## Body for folder 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 = this_folder.active_components + 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 + delete folder +
+
+
+ ${render_folder( folder )} +
+
+
+
diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako new file mode 100644 index 00000000000..d4d2d1fccfd --- /dev/null +++ b/templates/admin/library/libraries.mako @@ -0,0 +1,22 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Libraries +
+
+ Groups  |   + Users +
+ +
+ %if msg: +

${msg}

+ %endif +
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..67db5c1d1f6 --- /dev/null +++ b/templates/admin/library/library.mako @@ -0,0 +1,41 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Library +
+
+ Libraries  |   + Groups  |   + Users +
+ %if msg: +

${msg}

+ %endif +
Manage Library '${library.name}'
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ + +
 
+
+
+
+
+
+ Manage Root Folder | + delete library +
diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako new file mode 100644 index 00000000000..1c335678ffb --- /dev/null +++ b/templates/admin/library/new_dataset.mako @@ -0,0 +1,85 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Create New Library Dataset +
+
+ Libraries  |   + Groups  |   + Users +
+ %if msg: +

${msg}

+ %endif +
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/library/specified_users_group_libraries.mako b/templates/admin/library/specified_users_group_libraries.mako new file mode 100644 index 00000000000..37241869cf3 --- /dev/null +++ b/templates/admin/library/specified_users_group_libraries.mako @@ -0,0 +1,27 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Specified Users Group Libraries{ +<% + gn = unescape( group_name, unentities ) + email = unescape( user_email, unentities ) +%> +
+
+ Libraries  |   + Groups  |   + Users +
+
Libraries containing datasets associated with group '${gn}' that user '${email}' can access
+
+ %for library in libraries: + + %endfor +
+
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..a838d22a7ba 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 )"> @@ -15,11 +15,19 @@ +<% +if isinstance( data, trans.app.model.HistoryDatasetAssociation ): + id_name = 'id' +elif isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): + id_name = 'lid' +%> + +%if ( id_name == 'id' or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) ):
Edit Attributes
- +
- +
@@ -68,39 +76,41 @@

- <% converters = data.get_converter_types() %> - %if len( converters ) > 0: -

-
Convert to new format
-
- - -
- -
- + %if id_name == 'id': + <% converters = data.get_converter_types() %> + %if len( converters ) > 0: +
+
Convert to new format
+
+ + +
+ +
+ +
+ +
+ This will create a new dataset with the contents of this + dataset converted to a new format. +
+
- -
- This will create a new dataset with the contents of this - dataset converted to a new format. +
+
-
-
-
- -
- -
-
- -

+ +

+
+ +

+ %endif %endif @@ -108,7 +118,7 @@

Change data type
- +
+ +

+%else: +

+
View Attributes
+
+
+ Name: ${data.name} +
+ Info: ${data.info} +
+ Data Format: ${data.ext} +
+ %for element in metadata: + ${element.spec.desc}: ${element.value[0]} +
+ %endfor +
+
+
+ +

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

+
Change Dataset Access Permissions
+
+
+ +
+ <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% dataset_group_ids = [ assoc.group.id for assoc in data.dataset.groups ] %> +
+ Check each group which should have access to this dataset. +
+ %for group in user_groups: + %if group.id in dataset_group_ids: + <% assoc = filter( lambda x: x.group_id == group.id, data.dataset.groups )[0] %> + %else: + <% assoc = None %> + %endif + ${group.name}
+
+ %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
+ %endfor +
+ %endfor + +
+
+
+
+%elif trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ) ): +
+
Dataset Access Permissions
+
+
+ +
+ %for assoc in data.dataset.groups: + ${assoc.group.name} +
    + %for action in assoc.permitted_actions: +
  • ${trans.app.security_agent.get_permitted_action_description(action)}
  • + %endfor +
+ %endfor +
+ You are not allowed to change the permitted actions on this dataset. +
+
+
+
+%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..0058f39f2f1 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -17,8 +17,9 @@
  • Create a new empty history
  • %endif %if app.config.enable_beta_features: -
  • Construct workflow from the current history
  • +
  • Construct workflow from the current history
  • %endif +
  • Change default permitted actions for the current history
  • Share current history
  • %endif
  • Delete current history
  • diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako new file mode 100644 index 00000000000..013481d9351 --- /dev/null +++ b/templates/history/permissions.mako @@ -0,0 +1,66 @@ +<%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: + + checked + %else: + <% assoc = None %> + %endif + /> ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor + +
    + +
    + This will change the default permitted actions assigned + to new datasets in your current history. You may also + specify the defaults for new histories via the + user options. + +
    +
    +
    +
    + +
    +
    +
    +
    +%endif 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..5f5440c56a0 --- /dev/null +++ b/templates/library/library.mako @@ -0,0 +1,79 @@ +<%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 + # 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} + view or edit attributes +
    + + +## Render a folder +<%def name="render_folder( this_folder )"> +
    + Folder: ${this_folder.name} + <% + components = this_folder.active_components + 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..ab97adc6696 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -8,6 +8,7 @@ %else: diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako new file mode 100644 index 00000000000..e451bc71c29 --- /dev/null +++ b/templates/user/permissions.mako @@ -0,0 +1,65 @@ +<%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: + + checked + %else: + <% assoc = None %> + %endif + /> ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor + +
    + +
    + This will change the default permitted actions assigned + to new datasets in new histories. You may also specify + per-history defaults via the + history options. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index afb84c98a2d..c5a999d2532 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -10,6 +10,9 @@ import twill.commands as tc from twill.other_packages._mechanize_dist import ClientForm from elementtree import ElementTree +from galaxy.web.controllers.admin import entities, unentities +from xml.sax.saxutils import escape, unescape + buffer = StringIO.StringIO() #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose? @@ -31,7 +34,7 @@ class TwillTestCase( unittest.TestCase ): self.home() self.set_history() - """Functions associated with files""" + # Functions associated with files def files_diff( self, file1, file2 ): """Checks the contents of 2 files for differences""" if not filecmp.cmp( file1, file2 ): @@ -83,7 +86,7 @@ class TwillTestCase( unittest.TestCase ): errmsg += str( err ) raise AssertionError( errmsg ) - """Functions associated with histories""" + # Functions associated with histories def check_history_for_errors( self ): """Raises an exception if there are errors in a history""" self.visit_page( "history" ) @@ -210,7 +213,7 @@ class TwillTestCase( unittest.TestCase ): def view_stored_histories( self ): self.visit_page( "history_available" ) - """Functions associated with datasets (history items) and meta data""" + # Functions associated with datasets (history items) and meta data def get_job_stderr( self, id ): self.visit_page( "dataset/stderr?id=%s" % id ) return self.last_page() @@ -351,7 +354,7 @@ class TwillTestCase( unittest.TestCase ): genome_build = elem.get('dbkey') self.assertTrue( genome_build == dbkey ) - """Functions associated with user accounts""" + # Functions associated with user accounts def create( self, email='test@bx.psu.edu', password='testuser', confirm='testuser' ): self.visit_page( "user/create?email=%s&password=%s&confirm=%s" %(email, password, confirm) ) try: @@ -371,7 +374,7 @@ class TwillTestCase( unittest.TestCase ): self.check_page_for_string( "You are no longer logged in" ) self.home() #Reset our URL for future tests - """Functions associated with browsers, cookies, HTML forms and page visits""" + # Functions associated with browsers, cookies, HTML forms and page visits def check_page_for_string( self, patt ): """Looks for 'patt' in the current browser page""" page = self.last_page() @@ -488,7 +491,7 @@ class TwillTestCase( unittest.TestCase ): tc.go("%s" % url) tc.code( 200 ) - """Functions associated with Galaxy tools""" + # Functions associated with Galaxy tools def run_tool( self, tool_id, **kwd ): tool_id = tool_id.replace(" ", "+") """Runs the tool 'tool_id' and passes it the key/values from the *kwd""" @@ -511,3 +514,73 @@ class TwillTestCase( unittest.TestCase ): else: break self.assertNotEqual(count, maxiter) + + # Dataset Security stuff + def create_group( self, name='New Test Group', priority='10' ): + """Create a new group with 1 member""" + self.visit_url( "%s/admin/create_group" % self.url ) + form = tc.show() + self.check_page_for_string( "Create Group" ) + try: + tc.fv( "1", "name", name ) + tc.fv( "1", "priority", priority ) + # twill version 0.9 still does not allow for easily testing forms that contain + # multiple fields with the same name ( e.g., check boxes ). We could attempt to determine + # the number of the field on the form and use it instead of the name of the field, but we + # would be forced to drop and recreate the database every time we test in order to ensure + # the fields will be the same on the form ( since it is dynamically rendered from the database ). + tc.fv( "1", "3", "1" ) # 1-based form field 3 is the 1st check box named 'members', user id 1 is test@bx.psu.edu + tc.submit( "create_group_button" ) + except AssertionError, err: + errmsg = 'Exception caught attempting to create group: %s' % str( err ) + raise AssertionError( errmsg ) + self.home() + return form + def add_group_member( self, group_id='', group_name='' ): + """Add a member to an existing group""" + # twill version 0.9 does not allow for this test + self.visit_url( "%s/admin/group_members_edit?group_id=%s&group_name=%s" % ( self.url, group_id, group_name ) ) + self.check_page_for_string( 'Members of' ) + try: + tc.fv( "1", "", "2" ) # 1-based form field 2 is the 2nd check box named 'members', user id 2 is test2@bx.psu.edu + tc.submit( "group_members_edit_button" ) + except AssertionError, err: + errmsg = 'Exception caught attempting to create group: %s' % str( err ) + raise AssertionError( errmsg ) + self.home() + def mark_group_deleted( self, group_id='' ): + """Delete an existing group""" + self.visit_url( "%s/admin/mark_group_deleted?group_id=%s" % ( self.url, group_id ) ) + self.last_page() + self.check_page_for_string( 'The group has been marked as deleted' ) + self.home() + def undelete_group( self, group_id='' ): + """Undelete an existing group""" + self.visit_url( "%s/admin/undelete_group?group_id=%s" % ( self.url, group_id ) ) + self.last_page() + self.check_page_for_string( 'The group has been marked as not deleted' ) + self.home() + def purge_group( self, group=None ): + """Purge an existing group""" + if not group.deleted: + self.mark_group_deleted( group_id=group.id ) + group_id = str( group.id ) + group_name = group.name.replace( ' ', '+' ) + self.visit_url( "%s/admin/deleted_groups" % self.url ) + self.check_page_for_string( '%s' % group.name ) + self.visit_url( "%s/admin/purge_group?group_id=%s" % ( self.url, group_id ) ) + self.last_page() + self.check_page_for_string( 'The group has been purged from the database' ) + self.home() + + # Library stuff + def create_library( self, name='', description='' ): + """Create a new library""" + name = name.replace( ' ', '+' ) + description = description.replace( ' ', '+' ) + try: + self.visit_url( "%s/admin/library?name=%s&description=%s&create_library=None" % ( self.url, name, description ) ) + except AssertionError, err: + errmsg = 'Exception caught attempting to create library: %s' % str( err ) + raise AssertionError( errmsg ) + self.home() diff --git a/test/functional/__init__.py b/test/functional/__init__.py index 6943bcb5909..1413bb51171 100644 --- a/test/functional/__init__.py +++ b/test/functional/__init__.py @@ -73,6 +73,7 @@ def setup(): test_conf = "test.conf", log_destination = "stdout", use_heartbeat=False, + admin_users = 'test@bx.psu.edu', global_conf= { "__file__": "universe_wsgi.ini.sample" } ) log.info( "Embedded Universe application started" ) diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py new file mode 100644 index 00000000000..30b460a049e --- /dev/null +++ b/test/functional/test_security_and_libraries.py @@ -0,0 +1,91 @@ +import galaxy.model +from base.twilltestcase import TwillTestCase + +s = 'You must have Galaxy administrator privileges to use this feature.' + +class TestHistory( TwillTestCase ): + def test_00_admin_features_when_not_logged_in( self ): + """Testing admin_features when not logged in""" + self.logout() + self.visit_url( "%s/admin" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/reload_tool?tool_id=upload1" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/dataset_security" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/groups" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/create_group" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/group_members_edit" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/update_group_members" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/group_dataset_permitted_actions_edit" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/mark_group_deleted" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/purge_group" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/users" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/libraries" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/library" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/folder" % self.url ) + self.check_page_for_string( s ) + self.visit_url( "%s/admin/dataset" % self.url ) + self.check_page_for_string( s ) + def test_05_login_as_admin( self ): + """Testing logging in as an admin user""" + self.login( email='test@bx.psu.edu' ) #This is configured as our admin user + self.visit_page( "admin" ) + self.check_page_for_string( 'Galaxy Administration' ) + self.logout() + # Need to ensure that we have 2 users + self.login( email='test2@bx.psu.edu' ) # This will not be an admin user + self.visit_page( "admin" ) + self.check_page_for_string( s ) + def test_10_create_group( self ): + """Testing creating new group with 1 members""" + self.login( email='test@bx.psu.edu' ) + self.visit_page( "admin/groups" ) + # the following should have been created when account was created. + self.check_page_for_string( 'test@bx.psu.edu private group' ) + self.create_group( name='New Test Group', priority='10' ) + self.visit_page( "admin/groups" ) + self.check_page_for_string( "group_name=New+Test+Group" ) + # twill version 0.9 still does not allow for the following test + #def test_15_add_group_member( self ): + # """Testing adding a member to an existing group""" + # group = galaxy.model.Group.get_by( name='New Test Group' ) + # group_id = str( group.id ) + # group_name = group.name.replace( ' ', '+' ) + # self.add_group_member( group_id=group_id, group_name=group_name ) + # self.visit_page( 'admin/group_members?group_id=%s&group_name=%s' % ( group_id, group_name ) ) + # self.check_page_for_string( 'test@bx.psu.edu' ) + # self.check_page_for_string( 'test2@bx.psu.edu' ) + def test_20_delete_group( self ): + """Testing deleting a group""" + self.visit_page( "admin/groups" ) + self.check_page_for_string( "group_name=New+Test+Group" ) + group = galaxy.model.Group.get_by( name='New Test Group' ) + group_id = str( group.id ) + self.mark_group_deleted( group_id=group_id ) + def test_25_undelete_group( self ): + """Testing undeleting a deleted group""" + group = galaxy.model.Group.get_by( name='New Test Group' ) + group_id = str( group.id ) + self.undelete_group( group_id=group_id ) + def test_30_purge_group( self ): + """Testing purging a group""" + group = galaxy.model.Group.get_by( name='New Test Group' ) + self.purge_group( group=group ) + + def test_20_create_library( self ): + """Testing creating new library""" + self.create_library( name='New Test Library', description='New test Library Description' ) + self.visit_page( 'admin/libraries' ) + self.check_page_for_string( "New Test Library" ) + 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..19fb17fef95 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,7 @@ 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 ) + app.security_agent.set_dataset_permissions( 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..b5ccbf11c3c 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,7 @@ 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() + app.security_agent.set_dataset_permissions( 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..d28c10b73ca 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -27,12 +27,12 @@ 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 ) + app.security_agent.set_dataset_permissions( 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