diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 0c4ea728395..1964945012e 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -4,6 +4,7 @@ from galaxy import config, jobs, util, tools, web import galaxy.model import galaxy.model.mapping import galaxy.datatypes.registry +import galaxy.security class UniverseApplication( object ): """Encapsulates the state of a Universe application""" @@ -30,6 +31,8 @@ class UniverseApplication( object ): self.toolbox = tools.ToolBox( self.config.tool_config, self.config.tool_path, self ) #Load datatype converters self.datatypes_registry.load_datatype_converters( self.toolbox ) + #Load security policy + self.security_agent = self.model.security_agent # Start the job queue job_dispatcher = jobs.DefaultJobDispatcher( self ) self.job_queue = jobs.JobQueue( self, job_dispatcher ) diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 432499bef7a..ac54a889b7a 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -45,7 +45,7 @@ class Configuration( object ): self.job_scheduler_policy = kwargs.get("job_scheduler_policy", "FIFO") self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") ) self.job_working_directory = resolve_path( kwargs.get( "job_working_directory", "database/job_working_directory" ), self.root ) - self.admin_pass = kwargs.get('admin_pass',"galaxy") + self.admin_users = kwargs.get( "admin_users", "" ) self.sendmail_path = kwargs.get('sendmail_path',"/usr/sbin/sendmail") self.mailing_join_addr = kwargs.get('mailing_join_addr',"galaxy-user-join@bx.psu.edu") self.error_email_to = kwargs.get( 'error_email_to', None ) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 90d44ab47f4..7c58d38f632 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -110,7 +110,7 @@ class Gmaj( data.Data ): "nobutton": "false", "urlpause" :"100", "debug": "false", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey } ) + "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ) } class_name = "edu.psu.bx.gmaj.MajApplet.class" archive = "/static/gmaj/gmaj.jar" @@ -180,7 +180,7 @@ class Laj( data.Text ): "alignfile1": "display?id=%s" % dataset.id, "buttonlabel": "Launch LAJ", "title": "LAJ in Galaxy", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey } ), + "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ), "noseq": "true" } class_name = "edu.psu.cse.bio.laj.LajApplet.class" diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 7d08ccb2e46..4ef5a34744e 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -13,6 +13,7 @@ from galaxy import util import tempfile import galaxy.datatypes.registry from galaxy.datatypes.metadata import MetadataCollection +from galaxy.security import RBACAgent import logging log = logging.getLogger( __name__ ) @@ -33,13 +34,14 @@ class User( object ): self.external = False # Relationships self.histories = [] + def set_password_cleartext( self, cleartext ): """Set 'self.password' to the digest of 'cleartext'.""" self.password = sha.new( cleartext ).hexdigest() def check_password( self, cleartext ): """Check if 'cleartext' matches 'self.password' when hashed.""" return self.password == sha.new( cleartext ).hexdigest() - + class Job( object ): """ A job represents a request to run a tool given input datasets, tool @@ -101,252 +103,69 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -class HistoryDatasetAssociation( object ): - def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, - dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, - parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): - self.name = name or "Unnamed dataset" - self.id = id - self.hid = hid - self.info = info - self.blurb = blurb - self.peek = peek - self.extension = extension - self.dbkey = dbkey - self.designation = designation - self._metadata = metadata or dict() - self.deleted = deleted - self.visible = visible - # Relationships - self.history = history - if not dataset and create_dataset: - dataset = Dataset() - dataset.flush() +class GroupDatasetAssociation( object ): + def __init__( self, group, dataset, permitted_actions=[] ): + if isinstance( group, GroupDatasetAssociation ) or \ + isinstance( group, DefaultUserGroupAssociation ) or \ + isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.group = group + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset self.dataset = dataset - self.parent_id = parent_id - self.validation_errors = validation_errors - self.copied_from_history_dataset_association = copied_from_history_dataset_association - - @property - def ext( self ): - return self.extension - - @property - def states( self ): - return self.dataset.states - - def get_dataset_state( self ): - return self.dataset.state - def set_dataset_state ( self, state ): - self.dataset.state = state - self.dataset.flush() #flush here, because hda.flush() won't flush the Dataset object - state = property( get_dataset_state, set_dataset_state ) - - def get_file_name( self ): - return self.dataset.get_file_name() - - def set_file_name (self, filename): - return self.dataset.set_file_name( filename ) - - file_name = property( get_file_name, set_file_name ) - - @property - def extra_files_path( self ): - return self.dataset.extra_files_path - - @property - def datatype( self ): - return datatypes_registry.get_datatype_by_extension( self.extension ) + self.permitted_actions = permitted_actions + def add_permitted_action( self, action ): + if action not in self.permitted_actions: + return self.permitted_actions.append( action ) + raise 'action (%s) already exists in permitted actions list (%s: %s).' % ( action, str( self.id ), str( self.permitted_actions ) ) + def remove_permitted_action( self, action ): + return self.permitted_actions.remove( action ) - def get_metadata( self ): - if not self._metadata: - self._metadata = dict() - return MetadataCollection( self, self.datatype.metadata_spec ) - def set_metadata( self, bunch ): - # Needs to accept a MetadataCollection, a bunch, or a dict - self._metadata = dict( bunch.items() ) - metadata = property( get_metadata, set_metadata ) +class Group( object ): + public_id = None + permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) + def __init__( self, name = None, priority = 0 ): + self.name = name + self.priority = priority + @classmethod + def get_public_group( cls ): + return Group.get( cls.public_id ) + @classmethod + def set_public_group( cls, group ): + # We store the id instead of the object, because of alchemy sessions + if isinstance( group, Group ): + group = group.id + cls.public_id = group + @classmethod + def guess_public_group( cls ): + # Retrieve from database and store public group id + group = Group.select_by( name='public' )[0] + cls.set_public_group( group ) - """ - This provide backwards compatibility with using the old dbkey - field in the database. That field now maps to "old_dbkey" (see mapping.py). - """ - def get_dbkey( self ): - dbkey = self.metadata.dbkey - if not isinstance(dbkey, list): dbkey = [dbkey] - #if dbkey in [["?"], [None], []]: dbkey = [self.old_dbkey] - if dbkey in [[None], []]: return "?" - return dbkey[0] - def set_dbkey( self, value ): - if "dbkey" in self.datatype.metadata_spec: - if not isinstance(value, list): - self.metadata.dbkey = [value] - else: - self.metadata.dbkey = value - #if isinstance(value, list): - # self.old_dbkey = value[0] - #else: - # self.old_dbkey = value - dbkey = property( get_dbkey, set_dbkey ) - - def change_datatype( self, new_ext ): - self.clear_associated_files() - datatypes_registry.change_datatype( self, new_ext ) - def get_size( self ): - """Returns the size of the data on disk""" - return self.dataset.get_size() - def set_size( self ): - """Returns the size of the data on disk""" - return self.dataset.set_size() - def has_data( self ): - """Detects whether there is any data""" - return self.dataset.has_data() - def get_raw_data( self ): - """Returns the full data. To stream it open the file_name and read/write as needed""" - return self.datatype.get_raw_data( self ) - def write_from_stream( self, stream ): - """Writes data from a stream""" - self.datatype.write_from_stream(self, stream) - def set_raw_data( self, data ): - """Saves the data on the disc""" - self.datatype.set_raw_data(self, data) - def get_mime( self ): - """Returns the mime type of the data""" - return datatypes_registry.get_mimetype_by_extension( self.extension.lower() ) - def set_peek( self ): - return self.datatype.set_peek( self ) - def init_meta( self, copy_from=None ): - return self.datatype.init_meta( self, copy_from=copy_from ) - def set_meta( self, **kwd ): - self.clear_associated_files( metadata_safe = True ) - return self.datatype.set_meta( self, **kwd ) - def set_readonly_meta( self, **kwd ): - return self.datatype.set_readonly_meta( self, **kwd ) - def missing_meta( self ): - return self.datatype.missing_meta( self ) - def as_display_type( self, type, **kwd ): - return self.datatype.as_display_type( self, type, **kwd ) - def display_peek( self ): - return self.datatype.display_peek( self ) - def display_name( self ): - return self.datatype.display_name( self ) - def display_info( self ): - return self.datatype.display_info( self ) - def get_converted_files_by_type( self, file_type ): - valid = [] - for assoc in self.implicitly_converted_datasets: - if not assoc.deleted and assoc.type == file_type: - valid.append( assoc.dataset ) - return valid - def clear_associated_files( self, metadata_safe = False, purge = False ): - #metadata_safe = True means to only clear when assoc.metadata_safe == False - for assoc in self.implicitly_converted_datasets: - if not metadata_safe or not assoc.metadata_safe: - assoc.clear( purge = purge ) - def get_child_by_designation(self, designation): - for child in self.children: - if child.designation == designation: - return child - return None - - def get_converter_types(self): - return self.datatype.get_converter_types( self, datatypes_registry) - - def copy( self, copy_children = False, parent_id = None ): - des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self ) - des.flush() - if copy_children: - for child in self.children: - child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs - des.flush() - return des - - def add_validation_error( self, validation_error ): - self.validation_errors.append( validation_error ) - - def extend_validation_errors( self, validation_errors ): - self.validation_errors.extend(validation_errors) - - def mark_deleted( self, include_children=True ): - self.deleted = True - if include_children: - for child in self.children: - child.mark_deleted() - - - -class History( object ): - def __init__( self, id=None, name=None, user=None ): - self.id = id - self.name = name or "Unnamed history" - self.deleted = False - self.purged = False - self.genome_build = None - # Relationships +class UserGroupAssociation( object ): + def __init__( self, user, group ): self.user = user - self.datasets = [] - self.galaxy_sessions = [] - - def _next_hid( self ): - # TODO: override this with something in the database that ensures - # better integrity - if len( self.datasets ) == 0: - return 1 - else: - last_hid = 0 - for dataset in self.datasets: - if dataset.hid > last_hid: - last_hid = dataset.hid - return last_hid + 1 + self.group = group - def add_galaxy_session( self, galaxy_session, association=None ): - if association is None: - self.galaxy_sessions.append( GalaxySessionToHistoryAssociation( galaxy_session, self ) ) - else: - self.galaxy_sessions.append( association ) +class DefaultUserGroupAssociation( object ): + def __init__( self, user, group, permitted_actions ): + if isinstance( group, GroupDatasetAssociation ) or \ + isinstance( group, DefaultUserGroupAssociation ) or \ + isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.user = user + self.group = group + self.permitted_actions = permitted_actions - def add_dataset( self, dataset, parent_id=None, genome_build=None, set_hid = True ): - if isinstance( dataset, Dataset ): - dataset = HistoryDatasetAssociation( dataset = dataset ) - dataset.flush() - elif not isinstance( dataset, HistoryDatasetAssociation ): - raise TypeError, "You can only add Dataset and HistoryDatasetAssociation instances to a history." - if parent_id: - for data in self.datasets: - if data.id == parent_id: - dataset.hid = data.hid - break - else: - if set_hid: dataset.hid = self._next_hid() - else: - if set_hid: dataset.hid = self._next_hid() - dataset.history = self - if genome_build not in [None, '?']: - self.genome_build = genome_build - self.datasets.append( dataset ) - - def copy(self): - des = History() - des.flush() - des.name = self.name - des.user_id = self.user_id - for data in self.datasets: - new_data = data.copy( copy_children = True ) - des.add_dataset( new_data ) - new_data.flush() - des.hid_counter = self.hid_counter - des.flush() - return des - -# class Query( object ): -# def __init__( self, name=None, state=None, tool_parameters=None, history=None ): -# self.name = name or "Unnamed query" -# self.state = state -# self.tool_parameters = tool_parameters -# # Relationships -# self.history = history -# self.datasets = [] +class DefaultHistoryGroupAssociation( object ): + def __init__( self, history, group, permitted_actions ): + if isinstance( group, GroupDatasetAssociation ) or \ + isinstance( group, DefaultUserGroupAssociation ) or \ + isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.history = history + self.group = group + self.permitted_actions = permitted_actions class Dataset( object ): states = Bunch( NEW = 'new', @@ -356,6 +175,7 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) + permitted_actions = galaxy.security.get_permitted_actions( 'DATASET' ) file_path = "/tmp/" engine = None def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): @@ -440,8 +260,347 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) -class Old_Dataset( Dataset ): - pass +class DatasetInstance( object ): + """A base class for all 'dataset instances', HDAs, LDAs, etc""" + states = Dataset.states + permitted_actions = Dataset.permitted_actions + def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, + dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, + parent_id=None, validation_errors=None, visible=True, create_dataset = False ): + self.name = name or "Unnamed dataset" + self.id = id + self.info = info + self.blurb = blurb + self.peek = peek + self.extension = extension + self.dbkey = dbkey + self.designation = designation + self._metadata = metadata or dict() + self.deleted = deleted + self.visible = visible + # Relationships + if not dataset and create_dataset: + dataset = Dataset() + dataset.flush() + self.dataset = dataset + self.parent_id = parent_id + self.validation_errors = validation_errors + @property + def ext( self ): + return self.extension + def get_dataset_state( self ): + return self.dataset.state + def set_dataset_state ( self, state ): + self.dataset.state = state + self.dataset.flush() #flush here, because hda.flush() won't flush the Dataset object + state = property( get_dataset_state, set_dataset_state ) + def get_file_name( self ): + return self.dataset.get_file_name() + def set_file_name (self, filename): + return self.dataset.set_file_name( filename ) + file_name = property( get_file_name, set_file_name ) + @property + def extra_files_path( self ): + return self.dataset.extra_files_path + @property + def datatype( self ): + return datatypes_registry.get_datatype_by_extension( self.extension ) + def get_metadata( self ): + if not self._metadata: + self._metadata = dict() + return MetadataCollection( self, self.datatype.metadata_spec ) + def set_metadata( self, bunch ): + # Needs to accept a MetadataCollection, a bunch, or a dict + self._metadata = dict( bunch.items() ) + metadata = property( get_metadata, set_metadata ) + # This provide backwards compatibility with using the old dbkey + # field in the database. That field now maps to "old_dbkey" (see mapping.py). + def get_dbkey( self ): + dbkey = self.metadata.dbkey + if not isinstance(dbkey, list): dbkey = [dbkey] + #if dbkey in [["?"], [None], []]: dbkey = [self.old_dbkey] + if dbkey in [[None], []]: return "?" + return dbkey[0] + def set_dbkey( self, value ): + if "dbkey" in self.datatype.metadata_spec: + if not isinstance(value, list): + self.metadata.dbkey = [value] + else: + self.metadata.dbkey = value + #if isinstance(value, list): + # self.old_dbkey = value[0] + #else: + # self.old_dbkey = value + dbkey = property( get_dbkey, set_dbkey ) + def change_datatype( self, new_ext ): + self.clear_associated_files() + datatypes_registry.change_datatype( self, new_ext ) + def get_size( self ): + """Returns the size of the data on disk""" + return self.dataset.get_size() + def set_size( self ): + """Returns the size of the data on disk""" + return self.dataset.set_size() + def has_data( self ): + """Detects whether there is any data""" + return self.dataset.has_data() + def get_raw_data( self ): + """Returns the full data. To stream it open the file_name and read/write as needed""" + return self.datatype.get_raw_data( self ) + def write_from_stream( self, stream ): + """Writes data from a stream""" + self.datatype.write_from_stream(self, stream) + def set_raw_data( self, data ): + """Saves the data on the disc""" + self.datatype.set_raw_data(self, data) + def get_mime( self ): + """Returns the mime type of the data""" + return datatypes_registry.get_mimetype_by_extension( self.extension.lower() ) + def set_peek( self ): + return self.datatype.set_peek( self ) + def init_meta( self, copy_from=None ): + return self.datatype.init_meta( self, copy_from=copy_from ) + def set_meta( self, **kwd ): + self.clear_associated_files( metadata_safe = True ) + return self.datatype.set_meta( self, **kwd ) + def set_readonly_meta( self, **kwd ): + return self.datatype.set_readonly_meta( self, **kwd ) + def missing_meta( self ): + return self.datatype.missing_meta( self ) + def as_display_type( self, type, **kwd ): + return self.datatype.as_display_type( self, type, **kwd ) + def display_peek( self ): + return self.datatype.display_peek( self ) + def display_name( self ): + return self.datatype.display_name( self ) + def display_info( self ): + return self.datatype.display_info( self ) + def get_converted_files_by_type( self, file_type ): + valid = [] + for assoc in self.implicitly_converted_datasets: + if not assoc.deleted and assoc.type == file_type: + valid.append( assoc.dataset ) + return valid + def clear_associated_files( self, metadata_safe = False, purge = False ): + raise 'Unimplemented' + def get_child_by_designation(self, designation): + for child in self.children: + if child.designation == designation: + return child + return None + def get_converter_types(self): + return self.datatype.get_converter_types( self, datatypes_registry) + def add_validation_error( self, validation_error ): + self.validation_errors.append( validation_error ) + def extend_validation_errors( self, validation_errors ): + self.validation_errors.extend(validation_errors) + def mark_deleted( self, include_children=True ): + self.deleted = True + if include_children: + for child in self.children: + child.mark_deleted() + +class HistoryDatasetAssociation( DatasetInstance ): + def __init__( self, + hid = None, + history = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.hid = hid + # Relationships + self.history = history + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + def copy( self, copy_children = False, parent_id = None ): + des = HistoryDatasetAssociation( hid=self.hid, + name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_history_dataset_association=self ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def clear_associated_files( self, metadata_safe = False, purge = False ): + #metadata_safe = True means to only clear when assoc.metadata_safe == False + for assoc in self.implicitly_converted_datasets: + if not metadata_safe or not assoc.metadata_safe: + assoc.clear( purge = purge ) + +class History( object ): + def __init__( self, id=None, name=None, user=None ): + self.id = id + self.name = name or "Unnamed history" + self.deleted = False + self.purged = False + self.genome_build = None + # Relationships + self.user = user + self.datasets = [] + self.galaxy_sessions = [] + def _next_hid( self ): + # TODO: override this with something in the database that ensures + # better integrity + if len( self.datasets ) == 0: + return 1 + else: + last_hid = 0 + for dataset in self.datasets: + if dataset.hid > last_hid: + last_hid = dataset.hid + return last_hid + 1 + def add_galaxy_session( self, galaxy_session, association=None ): + if association is None: + self.galaxy_sessions.append( GalaxySessionToHistoryAssociation( galaxy_session, self ) ) + else: + self.galaxy_sessions.append( association ) + def add_dataset( self, dataset, parent_id=None, genome_build=None, set_hid = True ): + if isinstance( dataset, Dataset ): + dataset = HistoryDatasetAssociation( dataset = dataset ) + dataset.flush() + elif not isinstance( dataset, HistoryDatasetAssociation ): + raise TypeError, "You can only add Dataset and HistoryDatasetAssociation instances to a history." + if parent_id: + for data in self.datasets: + if data.id == parent_id: + dataset.hid = data.hid + break + else: + if set_hid: dataset.hid = self._next_hid() + else: + if set_hid: dataset.hid = self._next_hid() + dataset.history = self + if genome_build not in [None, '?']: + self.genome_build = genome_build + self.datasets.append( dataset ) + def copy( self, target_user = None ): + if not target_user: + target_user = self.user + des = History( user = target_user ) + des.flush() + des.name = self.name + for data in self.datasets: + new_data = data.copy( copy_children = True ) + des.add_dataset( new_data ) + new_data.flush() + des.hid_counter = self.hid_counter + des.flush() + return des + +class Library( object ): + def __init__( self, name = None, description = None, root_folder = None ): + self.name = name or "Unnamed library" + self.description = description + self.root_folder = root_folder + +class LibraryFolder( object ): + def __init__( self, name = None, description = None, item_count = 0, order_id = None ): + self.name = name or "Unnamed folder" + self.description = description + self.item_count = item_count + self.order_id = order_id + self.genome_build = None + def add_dataset( self, dataset, genome_build=None ): + dataset.folder_id = self.id + dataset.order_id = self.item_count + self.item_count += 1 + if genome_build not in [None, '?']: + self.genome_build = genome_build + def add_folder( self, folder ): + folder.parent_id = self.id + folder.order_id = self.item_count + self.item_count += 1 + +class LibraryFolderDatasetAssociation( DatasetInstance ): + def __init__( self, + folder = None, + order_id = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.folder = folder + self.order_id = order_id + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + def to_history_dataset_association( self, parent_id = None ): + des = HistoryDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self ) + des.flush() + for child in self.children: + child_copy = child.to_history_dataset_association( parent_id = des.id ) + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def copy( self, copy_children = False, parent_id = None ): + des = LibraryFolderDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def clear_associated_files( self, metadata_safe = False, purge = False ): + return + +class LibraryTag( object ): + def __init__( self, tag ): + self.tag = tag + +class LibraryTagFolderAssociation( object ): + def __init__( self, tag, folder ): + self.tag = tag + self.folder = folder + +class LibraryTagDatasetAssociation( object ): + def __init__( self, tag, dataset ): + self.tag = tag + self.dataset = dataset + +# class Query( object ): +# def __init__( self, name=None, state=None, tool_parameters=None, history=None ): +# self.name = name or "Unnamed query" +# self.state = state +# self.tool_parameters = tool_parameters +# # Relationships +# self.history = history +# self.datasets = [] + class ValidationError( object ): def __init__( self, message=None, err_type=None, attributes=None ): @@ -483,7 +642,16 @@ class Event( object ): self.message = message class GalaxySession( object ): - def __init__( self, id=None, user=None, remote_host=None, remote_addr=None, referer=None, current_history_id=None, session_key=None, is_valid=False, prev_session_id=None ): + def __init__( self, + id=None, + user=None, + remote_host=None, + remote_addr=None, + referer=None, + current_history_id=None, + session_key=None, + is_valid=False, + prev_session_id=None ): self.id = id self.user = user self.remote_host = remote_host diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cdc8419c439..cd4aa9c3ee9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -19,6 +19,7 @@ from sqlalchemy import * from galaxy.model import * from galaxy.model.custom_types import * from galaxy.util.bunch import Bunch +from galaxy.security import GalaxyRBACAgent metadata = DynamicMetaData( threadlocal=False ) context = SessionContext( create_session ) @@ -65,7 +66,6 @@ History.table = Table( "history", metadata, # Column( "state", String( 64 ) ), # Column( "tool_parameters", Pickle() ) ) - HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata, Column( "id", Integer, primary_key=True ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), @@ -73,6 +73,7 @@ HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id" ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), Column( "hid", Integer ), Column( "name", TrimmedString( 255 ) ), Column( "info", TrimmedString( 255 ) ), @@ -114,6 +115,107 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) +Group.table = Table( "galaxy_group", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "priority", Integer ), + Column( "deleted", Boolean, index=True, default=False ) ) + +UserGroupAssociation.table = Table( "user_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "permitted_actions", JSONType(), default=[] ) ) + +# The following table stores the permissions that are considered the defaults for new histories when they are created by a user +DefaultUserGroupAssociation.table = Table( "default_user_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "permitted_actions", JSONType(), default=[] ) ) + +# The following table stores the default permissions assigned to histories for datasets +# that need permissions ( dataset permissions that cannot be determined based on ancestor ) +DefaultHistoryGroupAssociation.table = Table( "default_history_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "permitted_actions", JSONType(), default=[] ) ) + +LibraryFolderDatasetAssociation.table = Table( "library_folder_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "order_id", Integer ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id", use_alter=True, name='history_dataset_association_dataset_id_fkey' ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id", use_alter=True, name='library_folder_dataset_association_id_fkey' ), nullable=True ), + Column( "name", TrimmedString( 255 ) ), + Column( "info", TrimmedString( 255 ) ), + Column( "blurb", TrimmedString( 255 ) ), + Column( "peek" , TEXT ), + Column( "extension", TrimmedString( 64 ) ), + Column( "metadata", MetadataType(), key="_metadata" ), + Column( "parent_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), + Column( "designation", TrimmedString( 255 ) ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "visible", Boolean ) ) + +Library.table = Table( "library", metadata, + Column( "id", Integer, primary_key=True ), + Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "description", TEXT ) ) + +LibraryFolder.table = Table( "library_folder", metadata, + Column( "id", Integer, primary_key=True ), + Column( "parent_id", Integer, ForeignKey( "library_folder.id" ), nullable = True, index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "description", TEXT ), + Column( "order_id", Integer ), + Column( "item_count", Integer ), + Column( "genome_build", TrimmedString( 40 ) ) ) + +LibraryTag.table = Table( "library_tag", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "text", TEXT ) ) + +LibraryTagFolderAssociation.table = Table( "library_tag_folder_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "library_tag.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +LibraryTagDatasetAssociation.table = Table( "library_tag_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "library_tag.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + Job.table = Table( "job", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -255,6 +357,10 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), @@ -268,7 +374,10 @@ assign_mapper( context, Dataset, Dataset.table, properties=dict( history_associations=relation( HistoryDatasetAssociation, - primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ) + primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ), + library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) ) ) ) @@ -298,6 +407,74 @@ assign_mapper( context, User, User.table, collection_class=ordering_list( 'order_index' ) ) ) ) +assign_mapper( context, Group, Group.table, + properties=dict( users=relation( UserGroupAssociation ), + datasets=relation( GroupDatasetAssociation ) ) ) + +assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, + properties=dict( user=relation( User, backref = "groups" ), + group=relation( Group, backref = "users" ) ) ) + +assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "groups" ), + group=relation( Group, backref = "datasets" ) ) ) + +assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, + properties=dict( user=relation( User, backref = "default_groups" ), + group=relation( Group ) ) ) + +assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssociation.table, + properties=dict( history=relation( History, backref = "default_groups" ), + group=relation( Group ) ) ) + +assign_mapper( context, Library, Library.table, + properties=dict( + root_folder=relation( LibraryFolder, + backref = backref( "library_root" ) ) + ) ) + +assign_mapper( context, LibraryFolder, LibraryFolder.table, + properties=dict( + folders=relation( + LibraryFolder, + primaryjoin=( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ), remote_side=[LibraryFolder.table.c.id] ) ), + tags=relation( + LibraryTagFolderAssociation, + primaryjoin=( LibraryFolder.table.c.id == LibraryTagFolderAssociation.table.c.folder_id ), + backref=backref( "folders" ) ) + ) ) + +assign_mapper( context, LibraryFolderDatasetAssociation, LibraryFolderDatasetAssociation.table, + properties=dict( + dataset=relation( Dataset ), + folder=relation( + LibraryFolder, + backref=backref( "datasets" ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_library_folder_dataset_association", primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + children=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + tags=relation( + LibraryTagDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.id == LibraryTagDatasetAssociation.table.c.dataset_id ), + backref=backref( "datasets" ) ) + ) ) + +assign_mapper( context, LibraryTag, LibraryTag.table ) + +assign_mapper( context, LibraryTagFolderAssociation, LibraryTagFolderAssociation.table, + properties=dict( tag=relation( LibraryTag ), + folder=relation( LibraryFolder ) ) ) + +assign_mapper( context, LibraryTagDatasetAssociation, LibraryTagDatasetAssociation.table, + properties=dict( tag=relation( LibraryTag ), + dataset=relation( LibraryFolderDatasetAssociation ) ) ) + assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -411,6 +588,41 @@ def init( file_path, url, engine_options={}, create_tables=False ): result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) result.context = context result.create_tables = create_tables + #load local galaxy security policy + result.security_agent = GalaxyRBACAgent( result ) + # TODO, Nate: The following may not work for our Galaxy instances because there are too + # many rows that need updating ( I think ) even though we have eliminated all of the + # Role stuff. Maybe we can test this to see how long it takes for about 1000 datasets. + # If we decide to use this approach rather than SQL commands to populate the tables, + # then this needs to be thoroughly tested to ensure the data is populated as expected + # (i.e., make sure naything that is public gets the public security settings, etc). + # + # Set up default table entries here, only exist for group access because + # permitted actions are exclusively restricted to the association between a group + # and a dataset + if result.Group.count() == 0: + log.warning( "There were no groups located, setting up default (public) group." ) + # Create public group + public_group = result.security_agent.create_group( name='public' ) + # Store public group id + result.security_agent.set_public_group( public_group ) + # Loop through all histories and set up rbac on users, histories and datasets + for history in result.History.select( result.History.table.c.purged == False ): + if history.user: + if not history.user.default_groups: + result.security_agent.setup_new_user( history.user ) + history.user.flush() + else: + result.security_agent.history_set_default_access( history, dataset=True ) + history.flush() + # Add all datasets which aren't in a history to the public group + orphans = result.Dataset.get_by( history_id = None ) + if orphans: + for dataset in orphans: + result.security_agent.set_dataset_groups( dataset, [ public_group ] ) + else: + result.security_agent.guess_public_group() + log.debug( "Public Group identified as id = %s." % ( Group.public_id ) ) return result def get_suite(): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..0eaba298d10 --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,245 @@ +""" +Galaxy Security + +""" +import logging +from galaxy.util.bunch import Bunch + +log = logging.getLogger(__name__) + +# TODO, Nate: Think about whether the following permitted actions are appropriate for the dataset and +# group objects. What should be the default "public" permitted actions? Make sure that the public group +# and public datasets are set with the correct permitted actions. Also make sure that "private" settings +# are correct when an authenticated user creates things inside their "private" environment. +class RBACAgent: + """Class that handles galaxy security""" + permitted_actions = Bunch( + # The ability to edit the metadata of the associated dataset + DATASET_EDIT_METADATA = 'dataset_edit_metadata', + # The ability to change the permissions of a dataset (so specifically, to add and modify + # group_dataset_association rows where the dataset is the dataset for which the permission is set). + DATASET_MANAGE_PERMISSIONS = 'dataset_manage_permissions', + # The ability to perform any read only operation on the dataset (view, display at external site, + # use in a job, etc). + DATASET_ACCESS = 'dataset_access' + ) + def allow_action( self, user, action, **kwd ): + raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + def guess_derived_groups_permitted_actions_for_datasets( self, datasets = [] ): + raise "Unimplemented Method" + def associate_components( self, **kwd ): + raise 'No valid method of associating provided components: %s' % kwd + def get_group( self, id ): + raise 'No valid method of retrieving group %s' % ( id ) + def create_group( self, **kwd ): + raise 'No valid method of creating group with %s' % ( kwd ) + def create_private_user_group( self, user ): + raise "Unimplemented Method" + def user_set_default_access( self, user, groups = None, history = False, dataset = False ): + raise "Unimplemented Method" + def setup_new_user( self, user ): + self.user_set_default_access( user, history = True, dataset = True ) + self.associate_components( user=user, group=self.get_public_group() ) + def history_set_default_access( self, history, groups=None, dataset=False ): + raise "Unimplemented Method" + def set_public_group( self, group ): + raise "Unimplemented Method" + def get_public_group( self ): + raise "Unimplemented Method" + def guess_public_group( self ): + raise "Unimplemented Method" + def set_dataset_groups( self, dataset, groups ): + raise "Unimplemented Method" + def set_dataset_permitted_actions( self, dataset ): + raise "Unimplemented Method" + def get_component_associations( self, **kwd ): + raise "Unimplemented Method" + def components_are_associated( self, **kwd ): + return bool( self.get_component_associations( **kwd ) ) + +class GalaxyRBACAgent( RBACAgent ): + def __init__( self, model, permitted_actions=None ): + self.model = model + if permitted_actions: + self.permitted_actions = permitted_actions + def allow_action( self, user, action, **kwd ): + if 'dataset' in kwd: + return self.allow_dataset_action( user, action, kwd['dataset'] ) + raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + def allow_dataset_action( self, user, action, dataset ): + """Returns true when user has permission to perform an action""" + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + # If dataset is in public group, we always return true for viewing and using + # This may need to change when the ability to alter groups and permitted_actions is allowed + if action == self.permitted_actions.DATASET_ACCESS and \ + self.components_are_associated( group = self.get_public_group(), dataset = dataset ): + return True + elif user is not None: + # Loop through permitted_actions and if allowed return true: + # Check permitted_actions associated with dataset through groups + for group_dataset_assoc in dataset.groups: + if self.components_are_associated( user = user, group = group_dataset_assoc.group ): + if action in group_dataset_assoc.permitted_actions: + return True + return False # No user and dataset not in public group, or user lacks permission + def guess_derived_groups_for_datasets( self, datasets=[] ): + # TODO, Nate: Make sure this method is functionally correct. + """Returns a list of groups for the output dataset based upon itself and provided datasets""" + access_groups = None + priority_access_group = None + for dataset in datasets: + # Determine access groups for output datasets - these groups are the + # intersection across all inputs. If we end up with no intersection + # between inputs, then we rely on priorities + if isinstance( dataset, self.model.HistoryDatasetAssociation ): + dataset = dataset.dataset + groups = [ data_group_assoc.group for data_group_assoc in dataset.groups ] + for group in groups: + if priority_access_group is None or priority_access_group.priority < group.priority: + priority_access_group = group + if access_groups is None: + access_groups = set( groups ) + else: + access_groups.intersection_update( set( groups ) ) + # Complete lists for output dataset access + if access_groups: + access_groups = list( access_groups) + else: + access_groups = [] + # If we have no groups left after intersection, take the highest priority group + if not access_groups: + if priority_access_group: + access_groups = [ priority_access_group ] + return access_groups + def get_group( self, id ): + return self.model.Group.get( id ) + raise 'No valid method of retrieving requested group %s' % ( id ) + def create_group( self, **kwd ): + rval = self.model.Group( **kwd ) + rval.flush() + return rval + raise 'No valid method of creating group with %s' % ( kwd ) + def associate_components( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to associate.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.associate_group_dataset( kwd['group'], kwd['dataset'] ) + elif 'user' in kwd: + if 'group' in kwd: + return self.associate_user_group( kwd['user'], kwd['group'] ) + raise 'No valid method of associating provided components: %s' % kwd + def disassociate_components( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to disassociate.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.disassociate_group_dataset( kwd['group'], kwd['dataset'] ) + raise 'No valid method of associating provided components: %s' % kwd + def associate_group_dataset( self, group, dataset, permitted_actions=[] ): + if not permitted_actions: + if isinstance( dataset.permitted_actions, Bunch ): + permitted_actions = dataset.permitted_actions.__dict__.values() + else: + permitted_actions = dataset.permitted_actions + assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) + assoc.flush() + return assoc + def disassociate_group_dataset( self, group, dataset ): + assoc = self.model.GroupDatasetAssociation.selectone_by( group_id = group.id, dataset_id = dataset.id ) + assoc.delete() + assoc.flush() + def associate_user_group( self, user, group ): + assoc = self.model.UserGroupAssociation( user, group ) + assoc.flush() + return assoc + def create_private_user_group( self, user ): + # Create private group + group_name = "%s private group" % user.email + group = self.model.Group( name=group_name, priority=10 ) + group.flush() + # Add user to group + self.associate_components( group=group, user=user ) + group.flush() + return group + def user_set_default_access( self, user, groups = None, history = False, dataset = False ): + # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. + if groups is None: + groups = [ self.create_private_user_group( user ) ] + if groups is not None: + for assoc in user.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + if isinstance( group, self.model.Group ): + permitted_actions = group.permitted_actions.__dict__.values() + else: + permitted_actions = group.permitted_actions + assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) + assoc.flush() + if history: + for history in user.histories: + self.history_set_default_access( history, groups=groups, dataset=dataset ) + def history_set_default_access( self, history, groups=None, dataset=False ): + # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. + if groups is None: + if history.user: + groups = [ assoc.group for assoc in history.user.default_groups ] + else: + groups = [ self.get_public_group() ] + if groups is not None: + for assoc in history.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + if isinstance( group, self.model.Group ): + permitted_actions = group.permitted_actions.__dict__.values() + else: + permitted_actions = group.permitted_actions + assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) + assoc.flush() + if dataset: + for data in history.datasets: + for hda in data.dataset.history_associations: + if history.user and hda.history not in history.user.histories: + self.set_dataset_groups( data.dataset, [ self.get_public_group() ] ) + break + else: + self.set_dataset_groups( data.dataset, groups ) + def get_public_group( self ): + return self.model.Group.get_public_group() + def set_public_group( self, group ): + return self.model.Group.set_public_group( group ) + def guess_public_group( self ): + return self.model.Group.guess_public_group() + def set_dataset_groups( self, dataset, groups ): + if isinstance( dataset, self.model.HistoryDatasetAssociation ): + dataset = dataset.dataset + for group_dataset_assoc in dataset.groups: + group_dataset_assoc.delete() + group_dataset_assoc.flush() + for group in groups: + if not isinstance( group, self.model.Group ): + group = group.group + self.associate_components( dataset=dataset, group=group ) + def get_component_associations( self, **kwd ): + # TODO, Nate: Make sure this method is functionally correct. + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.model.GroupDatasetAssociation.get_by( group_id = kwd['group'].id, dataset_id = kwd['dataset'].id ) + elif 'user' in kwd: + if 'group' in kwd: + return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) + raise 'No valid method of associating provided components: %s' % kwd + def dataset_has_group( self, dataset_id, group_id ): + return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) + +def get_permitted_actions( self, filter=None ): + '''Utility method to return a subset of RBACAgent's permitted actions''' + if filter is None: + return RBACAgent.permitted_actions + if not filter.endswith('_'): + filter += '_' + tmp_bunch = Bunch() + [tmp_bunch.__dict__.__setitem__(k, v) for k, v in RBACAgent.permitted_actions.items() if k.startswith(filter)] + return tmp_bunch diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index a56e6a829b4..7b5c4b0d305 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,6 +1085,8 @@ class Tool: else: visible = False ext = fields.pop(0).lower() child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + # TODO, Nate: Make sure the following is functionally correct. + self.app.security_agent.set_dataset_groups( child_dataset.dataset, outdata.dataset.groups ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1121,6 +1123,8 @@ class Tool: ext = fields.pop(0).lower() # Create new primary dataset primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + # TODO, Nate: Make sure the following is functionally correct. + self.app.security_agent.set_dataset_groups( primary_data.dataset, outdata.dataset.groups ) primary_data.flush() # Move data from temp location to dataset location shutil.move( filename, primary_data.file_name ) diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index f2bb0cd8185..bd74d188ee6 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -43,6 +43,9 @@ class DefaultToolAction( object ): assoc.flush() data = new_data break + # TODO, Nate: Make sure the permitted actions here are appropriate. + if data and not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset=data ): + raise "User does not have permission to use a dataset (%s) provided for input." % data.id return data if isinstance( input, DataToolParameter ): if isinstance( value, list ): @@ -79,6 +82,15 @@ class DefaultToolAction( object ): data = NoneDataset( datatypes_registry = trans.app.datatypes_registry ) if data.dbkey not in [None, '?']: input_dbkey = data.dbkey + + # Determine output dataset permitted_actions list + existing_datasets = [ inp for inp in inp_data.values() if inp ] + if existing_datasets: + output_access_groups = trans.app.security_agent.guess_derived_groups_for_datasets( existing_datasets ) + else: + # No valid inputs, we will use history defaults + output_access_groups = [ group.group for group in trans.history.default_groups ] + # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -120,6 +132,7 @@ class DefaultToolAction( object ): data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) # Commit the dataset immediately so it gets database assigned unique id data.flush() + trans.app.security_agent.set_dataset_groups( data.dataset, output_access_groups ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations @@ -183,6 +196,9 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: + # TODO, Nate: Make sure the permitted actions here are appropriate. + if not trans.app.security_agent.allow_action( trans.user, dataset.permitted_actions.DATASET_ACCESS, dataset=dataset ): + raise "User does not have permission to use a dataset (%s) provided for input." % data.id job.add_input_dataset( name, dataset ) else: job.add_input_dataset( name, None ) diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index b3904436a63..a9623ae40e0 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,8 +65,10 @@ class UploadToolAction( object ): return dict( output=data_list[0] ) def upload_empty(self, trans, err_code, err_msg): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) - data.name = err_code + data = trans.app.model.HistoryDatasetAssociation( create_dataset=True ) + # TODO, Nate: Make sure the following is appropriate. + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + data.name = err_code data.extension = "txt" data.dbkey = "?" data.info = err_msg @@ -85,12 +87,12 @@ class UploadToolAction( object ): if not os.path.getsize( temp_name ) > 0: raise BadFileException( "you attempted to upload an empty file." ) - # See if we have a gzipped file, which, if it passes our restrictions, we'll decompress on the fly. + # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress on the fly. is_gzipped, is_valid = self.check_gzip( temp_name ) if is_gzipped and not is_valid: raise BadFileException( "you attempted to upload an inappropriate file." ) elif is_gzipped and is_valid: - #We need to decompress the temp_name file + # We need to uncompress the temp_name file CHUNK_SIZE = 2**20 # 1Mb fd, uncompressed = tempfile.mkstemp() gzipped_file = gzip.GzipFile( temp_name ) @@ -159,6 +161,8 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + # TODO, Nate: Make sure the following is appropriate. + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) data.name = file_name data.dbkey = dbkey data.info = info diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index b10b51bdb5e..b626e665472 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -972,6 +972,8 @@ class DrillDownSelectToolParameter( ToolParameter ): class DataToolParameter( ToolParameter ): + # TODO, Nate: Make sure the following unit tests appropriately test the dataset security + # components. Add as many additional tests as necessary. """ Parameter that takes on one (or many) or a specific set of values. @@ -979,19 +981,35 @@ class DataToolParameter( ToolParameter ): displayed as radio buttons and multiple selects as a set of checkboxes >>> # Mock up a history (not connected to database) - >>> from galaxy.model import History, HistoryDatasetAssociation + >>> from galaxy.model import History, HistoryDatasetAssociation, User, Group >>> from galaxy.util.bunch import Bunch + >>> from galaxy.security import GalaxyRBACAgent + >>> import galaxy.model + >>> security_agent = GalaxyRBACAgent( galaxy.model ) >>> hist = History() >>> hist.flush() - >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) ) + >>> group = Group( 'test' ) + >>> group.flush() + >>> Group.public_id = group.id + >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset1, [ group ] ) + >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset2, [ group ] ) + >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset3, [ group ] ) + >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset4, [ group ] ) + >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) + >>> security_agent.set_dataset_groups( dataset5, [ group ] ) + >>> hist.add_dataset( dataset1 ) + >>> hist.add_dataset( dataset2 ) + >>> hist.add_dataset( dataset3 ) + >>> hist.add_dataset( dataset4 ) + >>> hist.add_dataset( dataset5 ) >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah - >>> print p.get_html( trans=Bunch( history=hist ) ) + >>> print p.get_html( trans=Bunch( history=hist, user=None, app=Bunch( security_agent = security_agent ) ) ) 2: Unnamed dataset 5: Unnamed dataset @@ -1047,7 +1065,7 @@ class DataToolParameter( ToolParameter ): hid = "%s.%d" % ( parent_hid, i + 1 ) else: hid = str( data.hid ) - if not data.deleted and data.state not in [data.states.ERROR] and data.visible: + if not data.deleted and data.state not in [data.states.ERROR] and data.visible and trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): if self.options and data.get_dbkey() != filter_value: continue if isinstance( data.datatype, self.formats): @@ -1061,6 +1079,8 @@ class DataToolParameter( ToolParameter ): data = datasets[0] elif not self.converter_safe( other_values, trans ): continue + if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + continue selected = ( value and ( data in value ) ) field.add_option( "%s: (as %s) %s" % ( hid, target_ext, data.name[:30] ), data.id, selected ) break #we only report the first valid converter, assume self.extensions is a priority list diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index fd9b4be90d8..f0ccc2f4f07 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,24 +1,796 @@ +import shutil, StringIO, operator +from galaxy import util from galaxy.web.base.controller import * -import logging, sets, time +from galaxy.datatypes import sniff +from galaxy.security import RBACAgent +import galaxy.model +from xml.sax.saxutils import escape, unescape +import pkg_resources +pkg_resources.require( "sqlalchemy>=0.3" ) +import sqlalchemy as sa +import logging log = logging.getLogger( __name__ ) +entities = { '@': 'FuNkYaT' } +unentities = { 'FuNkYaT' : '@' } +no_privilege_msg = "You must have Galaxy administrator privileges to use this feature." + class Admin( BaseController ): + def user_is_admin( self, trans ): + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + if not admin_users: + return False + user = trans.get_user() + if not user: + return False + if not user.email in admin_users: + return False + return True @web.expose def index( self, trans, **kwd ): - msg = '' - if 'action' in kwd: - if kwd['action'] == "tool_reload": - msg = self.tool_reload( **kwd ) - return trans.fill_template( 'admin_main.mako', toolbox=self.app.toolbox, msg=msg ) - - def tool_reload( self, tool_version=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) - if params.passwd==self.app.config.admin_pass: - tool_id = params.tool_id - self.app.toolbox.reload( tool_id ) - msg = 'Reloaded tool: ' + tool_id + msg = params.msg + return trans.fill_template( '/admin/index.mako', msg=msg ) + @web.expose + def reload_tool( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg ) + @web.expose + def tool_reload( self, trans, tool_version=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + tool_id = params.tool_id + self.app.toolbox.reload( tool_id ) + msg = 'Reloaded tool: ' + tool_id + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg ) + @web.expose + def dataset_security( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/dataset_security/index.mako', msg=msg ) + + # Galaxy Group Stuff + @web.expose + def groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # This query retrieves groups that are not deleted and members of each group + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ), + ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), + sa.func.count( galaxy.model.User.table.c.id ).label( 'total_members' ) ), + whereclause = galaxy.model.Group.table.c.deleted == False, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.UserGroupAssociation.table + ).outerjoin( galaxy.model.User.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.Group.table.c.name, + galaxy.model.Group.table.c.priority ], + order_by = [ galaxy.model.Group.table.c.name ] ) + groups = [] + for row in q.execute(): + # This 2nd query retrieves the number of datasets and dataset permitted_actions associated with each group + q2 = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ), + sa.func.count( galaxy.model.Dataset.table.c.id ).label( 'total_datasets' ) ), + whereclause = galaxy.model.Group.table.c.id == row.group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table + ).outerjoin( galaxy.model.Dataset.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) + for row2 in q2.execute(): + total_datasets = row2.total_datasets + permitted_actions = [] + # There may not yet be any GroupDatasetAssociations, in which case no + # actions will be found + if row2.permitted_actions: + for action in row2.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + groups.append( ( row.group_id, + escape( row.group_name, entities ), + row.group_priority, + row.total_members, + total_datasets, + permitted_actions ) ) + return trans.fill_template( '/admin/dataset_security/groups.mako', + groups=groups, + msg=msg ) + @web.expose + def create_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email') ), + from_obj = [ galaxy.model.User.table ], + order_by = [ galaxy.model.User.table.c.email ] ) + users = [] + for row in q.execute(): + users.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/group_create.mako', users=users, msg=msg ) + @web.expose + def new_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + name = unescape( params.name, unentities ) + if not name: + msg = "Please enter a name" + trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) else: - msg = 'Invalid password' - return msg + try: + priority = int( params.priority ) + except: + priority = 0 + # Create the group + group = galaxy.model.Group( name, priority ) + group.flush() + # Add the members + members = params.members + for user_id in members: + user = galaxy.model.User.get( user_id ) + # Create the UserGroupAssociation + user_group_association = galaxy.model.UserGroupAssociation( user, group ) + user_group_association.flush() + msg = "The new group has been created with priority %s and %s members" % ( str( priority ), str( len( members ) ) ) + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def group_members( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group_name = unescape( params.group_name, unentities ) + # This query retrieves all members of the group + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email' ) ), + whereclause = galaxy.model.UserGroupAssociation.table.c.group_id == group_id, + from_obj = [ sa.outerjoin( galaxy.model.UserGroupAssociation.table, + galaxy.model.User.table ) ], + order_by = [ 'user_email' ] ) + members = [] + for row in q.execute(): + members.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/group_members.mako', + group_id=group_id, + group_name=escape( group_name, entities ), + members=members, + msg=msg ) + @web.expose + def group_members_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group_name = unescape( params.group_name, unentities ) + members = params.members + # First get all users + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email' ) ), + order_by = [ 'user_email' ] ) + users = [] + for row in q.execute(): + users.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + # Then get members of the group + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email' ) ), + whereclause = galaxy.model.UserGroupAssociation.table.c.group_id == group_id, + from_obj = [ sa.outerjoin( galaxy.model.UserGroupAssociation.table, + galaxy.model.User.table ) ], + order_by = [ 'user_email' ] ) + members = [] + for row in q.execute(): + members.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/group_members_edit.mako', + group_id=group_id, + group_name=escape( group_name, entities ), + users=users, + members=members, + msg=msg ) + @web.expose + def update_group_members( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + group_id = int( params.group_id ) + members = params.members + if members and not isinstance( members, list ): + # mako passes singleton lists as strings for some reason + members = [ members ] + # Handle case where admin removed all members from group + elif members is None: + members = [] + group = galaxy.model.Group.get( group_id ) + # This is tricky since we have default association tables with + # records referring to members of this group. Because of this, + # we'll need to handle changes to the member list rather than the + # simpler approach of deleting all existing members and creating + # new records for user_ids in the received members param. + # First remove existing members that are not in the received members param + for user_group_assoc in group.users: + if user_group_assoc.user_id not in members: + user = galaxy.model.User.get( user_group_assoc.user_id ) + # Delete DefaultUserGroupAssociations + for default_user_group_association in user.default_groups: + if default_user_group_association.group_id == group_id: + default_user_group_association.delete() + default_user_group_association.flush() + break # Should only be 1 record + # Delete DefaultHistoryGroupAssociations + for history in user.histories: + for default_history_group_association in history.default_groups: + if default_history_group_association.group_id == group_id: + default_history_group_association.delete() + default_history_group_association.flush() + # Delete the UserGroupAssociation + user_group_assoc.delete() + user_group_assoc.flush() + # Then add all new members to the group + for user_id in members: + user = galaxy.model.User.get( user_id ) + if user not in group.users: + user_group_association = galaxy.model.UserGroupAssociation( user, group ) + user_group_association.flush() + msg = "Group membership has been updated with a total of %s members" % len( members ) + trans.response.send_redirect( '/admin/group_members?group_id=%s&group_name=%s&msg=%s' % ( str( group_id ), params.group_name, msg ) ) + @web.expose + def group_dataset_permitted_actions( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = int( params.group_id ) + group_name = unescape( params.group_name, unentities ) + # Need to get all actions to send to the form + dataset_actions = [] + dpas = RBACAgent.permitted_actions + for dpa in dpas.items(): + if dpa[0].startswith( 'DATASET' ): + dataset_actions.append( dpa[1] ) + dataset_actions.sort() + q = sa.select( ( ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ) ), + whereclause = galaxy.model.GroupDatasetAssociation.table.c.id == group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table ) ] ) + gdas = [] + for row in q.execute(): + permitted_actions = [] + # Although there may be GroupDatasetAssociations, there may not be any permitted_actions on them + if row.permitted_actions: + for action in row.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + gdas.append( ( row.group_priority, + permitted_actions ) ) + break # Just need 1 row + return trans.fill_template( '/admin/dataset_security/group_dataset_permitted_actions_edit.mako', + group_id=group_id, + group_name=escape( group_name, entities ), + gdas=gdas, + dataset_actions=dataset_actions, + msg=msg ) + @web.expose + def group_dataset_permitted_actions_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + group_id = int( params.group_id ) + actions = params.actions + if actions and not isinstance( actions, list ): + actions = [ actions ] + # Update the permitted_actions for every GroupDatasetAssociation of the Group + q = sa.update( galaxy.model.GroupDatasetAssociation.table, + whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, + values = { galaxy.model.GroupDatasetAssociation.table.c.permitted_actions : actions } ) + result = q.execute() + msg = "The dataset permitted actions for the group have been updated, affecting %d rows in the group_dataset_association table" % result.rowcount + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def mark_group_deleted( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group = galaxy.model.Group.get( group_id ) + group.deleted = True + group.flush() + msg = "The group has been marked as deleted." + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def deleted_groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # This query retrieves groups that are not deleted and members of each group + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ), + ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), + sa.func.count( galaxy.model.User.table.c.id ).label( 'total_members' ) ), + whereclause = galaxy.model.Group.table.c.deleted == True, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.UserGroupAssociation.table + ).outerjoin( galaxy.model.User.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.Group.table.c.name, + galaxy.model.Group.table.c.priority ], + order_by = [ galaxy.model.Group.table.c.name ] ) + groups = [] + for row in q.execute(): + # This 2nd query retrieves the number of datasets and dataset permitted_actions associated with each group + q2 = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ), + sa.func.count( galaxy.model.Dataset.table.c.id ).label( 'total_datasets' ) ), + whereclause = galaxy.model.Group.table.c.id == row.group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table + ).outerjoin( galaxy.model.Dataset.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) + for row2 in q2.execute(): + total_datasets = row2.total_datasets + permitted_actions = [] + # There may not yet be any GroupDatasetAssociations, in which case no + # actions will be found + if row2.permitted_actions: + for action in row2.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + groups.append( ( row.group_id, + escape( row.group_name, entities ), + row.group_priority, + row.total_members, + total_datasets, + permitted_actions ) ) + return trans.fill_template( '/admin/dataset_security/deleted_groups.mako', + groups=groups, + msg=msg ) + @web.expose + def undelete_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group = galaxy.model.Group.get( group_id ) + group.deleted = False + group.flush() + msg = "The group has been marked as not deleted." + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def purge_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group = galaxy.model.Group.get( group_id ) + # Remove members and all associations + for user_group_assoc in group.users: + user = galaxy.model.User.get( user_group_assoc.user_id ) + # Delete DefaultUserGroupAssociations + for default_user_group_association in user.default_groups: + if default_user_group_association.group_id == group_id: + default_user_group_association.delete() + default_user_group_association.flush() + break # Should only be 1 record + # Delete DefaultHistoryGroupAssociations + for history in user.histories: + for default_history_group_association in history.default_groups: + if default_history_group_association.group_id == group_id: + default_history_group_association.delete() + default_history_group_association.flush() + # Delete the UserGroupAssociation + user_group_assoc.delete() + user_group_assoc.flush() + # Delete the Group + group.delete() + group.flush() + msg = "The group has been purged from the database." + trans.response.send_redirect( '/admin/deleted_groups?msg=%s' % msg ) + + # Galaxy User Stuff + @web.expose + def users( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email') ), + from_obj = [ galaxy.model.User.table ], + order_by = [ galaxy.model.User.table.c.email ] ) + users = [] + for row in q.execute(): + users.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/users.mako', + users=users, + msg=msg ) + @web.expose + def specified_users_groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + user_id = int( params.user_id ) + user_email = unescape( params.user_email, unentities ) + # Get the groups to which the user belongs + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ), + ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ) ), + whereclause = galaxy.model.User.table.c.id == user_id, + from_obj = [ sa.outerjoin( galaxy.model.User.table, + galaxy.model.UserGroupAssociation.table ).outerjoin( galaxy.model.Group.table ) ], + order_by = [ 'group_name' ] ) + groups = [] + for row in q.execute(): + # Perform a 2nd query to get datasets associated with each group + q2 = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ), + sa.func.count( galaxy.model.Dataset.table.c.id ).label( 'total_datasets' ) ), + whereclause = galaxy.model.Group.table.c.id == row.group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table + ).outerjoin( galaxy.model.Dataset.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) + for row2 in q2.execute(): + total_datasets = row2.total_datasets + permitted_actions = [] + # There may not yet be any GroupDatasetAssociations, in which case no + # actions will be found + if row2.permitted_actions: + for action in row2.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + groups.append( ( row.group_id, + escape( row.group_name, entities ), + row.group_priority, + row2.total_datasets, + permitted_actions ) ) + return trans.fill_template( '/admin/dataset_security/specified_users_groups.mako', + user_id=user_id, + user_email=escape( user_email, entities ), + groups=groups, + msg=msg ) + + # Galaxy Library Stuff + @web.expose + def libraries( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + return trans.fill_template( '/admin/library/libraries.mako', libraries=trans.app.model.Library.select() ) + @web.expose + def library( self, trans, id=None, name="Unnamed", description=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if 'create_library' in kwd: + library = trans.app.model.Library( name=name, description=description ) + root_folder = trans.app.model.LibraryFolder( name=name, description=description ) + root_folder.flush() + library.root_folder = root_folder + library.flush() + trans.response.send_redirect( web.url_for( action='folder', id = root_folder.id ) ) + elif id is None: + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Library", name = "create_library", submit_text = "Submit" ) + .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) + .add_text( name = "description", label = "Description", value = None, error = None, help = None ) + .add_input( 'hidden', "Create Library", 'create_library', use_label = False ) ) + library = trans.app.model.Library.get( id ) + if library: + return trans.fill_template( '/admin/library/library.mako', library = library ) + else: + return trans.show_error_message( "Invalid library specified" ) + @web.expose + def folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if 'create_folder' in kwd: + folder = trans.app.model.LibraryFolder( name = name, description = description ) + # We are associating the last used genome_build with folders, so we will always + # initialize a new folder with the first dbkey in util.dbnames which is currently + # ? unspecified (?) + folder.genome_build = util.dbnames.default_value + if parent_id: + parent_folder = trans.app.model.LibraryFolder.get( parent_id ) + parent_folder.add_folder( folder ) + folder.flush() + trans.response.send_redirect( web.url_for( action='folder', id = folder.id ) ) + elif id is None: + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Folder", name = "create_folder", submit_text = "Submit" ) + .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) + .add_text( name = "description", label = "Description", value = None, error = None, help = None ) + .add_input( 'hidden', None, 'parent_id', value = parent_id, use_label = False ) + .add_input( 'hidden', "Create Folder", 'create_folder', use_label = False ) ) + folder = trans.app.model.LibraryFolder.get( id ) + if folder: + msg = '' + if 'rename_folder' in kwd: + folder.name = name + folder.description = description + folder.flush() + msg = 'Folder has been renamed.' + return trans.fill_template( '/admin/library/folder.mako', folder=folder, msg=msg ) + else: + return trans.show_error_message( "Invalid folder specified" ) + @web.expose + def dataset( self, trans, id=None, name="Unnamed", info='no info', extension=None, folder_id=None, dbkey=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if isinstance( dbkey, list ): + last_used_build = dbkey[0] + else: + last_used_build = dbkey + if folder_id and not last_used_build: + folder = trans.app.model.LibraryFolder.get( folder_id ) + last_used_build = folder.genome_build + data_files = [] + + # add_file method + def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): + data_type = None + temp_name = sniff.stream_to_file( file_obj ) + if space_to_tab: + line_count = sniff.convert_newlines_sep2tabs( temp_name ) + else: + line_count = sniff.convert_newlines( temp_name ) + if extension == 'auto': + data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) + else: + data_type = extension + dataset = trans.app.model.LibraryFolderDatasetAssociation( name=name, + info=info, + extension=data_type, + dbkey=dbkey, + create_dataset=True ) + folder = trans.app.model.LibraryFolder.get( folder_id ) + folder.add_dataset( dataset, genome_build=last_used_build ) + dataset.flush() + # GroupDatasetAssociations will enable security on the dataset based on the permitted_actions + # associated with the GroupDatasetAssociation. The default permitted_actions at this point + # will be DATASET_ACCESS, but the user can change this after the file is uploaded. + permitted_actions = [ RBACAgent.permitted_actions.DATASET_ACCESS ] + for group_id in groups: + group = galaxy.model.Group.get( group_id ) + group_dataset_assoc = galaxy.model.GroupDatasetAssociation( group, dataset.dataset, permitted_actions ) + group_dataset_assoc.flush() + shutil.move( temp_name, dataset.dataset.file_name ) + dataset.dataset.state = dataset.dataset.states.OK + dataset.init_meta() + if line_count is not None: + try: + dataset.set_peek( line_count=line_count ) + except: + dataset.set_peek() + else: + dataset.set_peek() + dataset.set_size() + if dataset.missing_meta(): + dataset.datatype.set_meta( dataset ) + trans.app.model.flush() + return dataset + # END add_file method + + if 'create_dataset' in kwd: + # Copied from upload tool action + last_dataset_created = None + data_file = kwd['file_data'] + url_paste = kwd['url_paste'] + space_to_tab = False + if 'space_to_tab' in kwd: + if kwd['space_to_tab'] not in ["None", None]: + space_to_tab = True + groups = kwd['groups'] + if groups and not isinstance( groups, list ): + # mako sends singleton lists as a string + groups = [ groups ] + if groups is None: + groups = [] + temp_name = "" + data_list = [] + + if 'filename' in dir( data_file ): + file_name = data_file.filename + file_name = file_name.split( '\\' )[-1] + file_name = file_name.split( '/' )[-1] + last_dataset_created = add_file( data_file.file, + file_name, + extension, + dbkey, + last_used_build, + groups, + info="uploaded file", + space_to_tab=space_to_tab ) + elif url_paste not in [ None, "" ]: + if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + url_paste = url_paste.replace( '\r', '' ).split( '\n' ) + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + last_dataset_created = add_file( urllib.urlopen( line ), + line, + extension, + dbkey, + last_used_build, + groups, + info="uploaded url", + space_to_tab=space_to_tab ) + else: + is_valid = False + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + is_valid = True + break + if is_valid: + last_dataset_created = add_file( StringIO.StringIO( url_paste ), + 'Pasted Entry', + extension, + dbkey, + last_used_build, + groups, + info="pasted entry", + space_to_tab=space_to_tab ) + trans.response.send_redirect( web.url_for( action='dataset', id=last_dataset_created.id ) ) + elif id is None: + # Send list of data formats to the form so the "extension" select list can be populated dynamically + file_formats = trans.app.datatypes_registry.upload_file_formats + # Send list of genome builds to the form so the "dbkey" select list can be populated dynamically + def get_dbkey_options( last_used_build ): + for dbkey, build_name in util.dbnames: + yield build_name, dbkey, ( dbkey==last_used_build ) + dbkeys = get_dbkey_options( last_used_build ) + # Send list of groups to the form so the dataset can be associated with 1 or more of them. + groups = [] + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ) ), + order_by = [ galaxy.model.Group.table.c.name ] ) + for row in q.execute(): + groups.append( ( row.group_id, row.group_name ) ) + groups = sorted( groups, key=operator.itemgetter(1) ) + return trans.fill_template( '/admin/library/new_dataset.mako', + folder_id=folder_id, + file_formats=file_formats, + dbkeys=dbkeys, + last_used_build=last_used_build, + groups=groups ) + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if dataset: + # Copied from edit attributes for 'regular' datasets with some additions + p = util.Params(kwd, safe=False) + if p.change_permitted_actions: + # The user clicked the Save button on the 'Group Associations' form + actions = p.actions + if actions and not isinstance( actions, list ): + actions = [ actions ] + if actions is None: + actions = [] + # actions is a list of comma-separated strings consisting of group_id and permitted_action, + # something like: ['6,dataset_access', '6,dataset_edit_metadata']. We'll parse them and + # create a dict whose keys are groups_id and values are permitted_actions + gdpa_dict = {} + for action in actions: + group_id, dpa = action.split( ',' ) + group_id = int( group_id ) + if group_id in gdpa_dict.keys(): + gdpa_dict[ group_id ].append( dpa ) + else: + gdpa_dict[ group_id ] = [ dpa ] + # Refresh the Dataset to ensure we have a valid set of DatasetGroupAssociations + dataset.dataset.refresh() + # Check to see if we need to delete any GroupDatasetAssociations. This occurs if + # the user unchecked all boxes for a group + for group_dataset_assoc in dataset.dataset.groups: + if group_dataset_assoc.group_id not in gdpa_dict.keys(): + group_dataset_assoc.delete() + group_dataset_assoc.flush() + # Use the dict to update the permitted actions for each GroupDatasetAssociaton + for group_id in gdpa_dict: + actions = gdpa_dict[ group_id ] + # Update the permitted_actions for every GroupDatasetAssociation of the Group + q = sa.update( galaxy.model.GroupDatasetAssociation.table, + whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, + values = { galaxy.model.GroupDatasetAssociation.table.c.permitted_actions : actions } ) + result = q.execute() + elif p.change: + # The user clicked the Save button on the 'Change data type' form + trans.app.datatypes_registry.change_datatype( dataset, p.datatype ) + trans.app.model.flush() + elif p.save: + # The user clicked the Save button on the 'Edit Attributes' form + dataset.name = name + dataset.info = info + # The following for loop will save all metadata_spec items + for name, spec in dataset.datatype.metadata_spec.items(): + if spec.get("readonly"): + continue + optional = p.get("is_"+name, None) + if optional and optional == 'true': + # optional element... == 'true' actually means it is NOT checked (and therefore ommitted) + setattr(dataset.metadata,name,None) + else: + setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) + + dataset.datatype.after_edit( dataset ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + elif p.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + for name, spec in dataset.datatype.metadata_spec.items(): + # We need to be careful about the attributes we are resetting + if name != 'name' and name != 'info' and name != 'dbkey': + if spec.get( 'default' ): + setattr( dataset.metadata,name,spec.unwrap( spec.get( 'default' ), spec )) + dataset.datatype.set_meta( dataset ) + dataset.datatype.after_edit( dataset ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + + dataset.datatype.before_edit( dataset ) + # Get all actions to send to the form + dataset_actions = [] + dpas = RBACAgent.permitted_actions + for dpa in dpas.items(): + if dpa[0].startswith( 'DATASET' ): + dataset_actions.append( dpa[1] ) + dataset_actions.sort() + # Get the permitted_actions of each GroupDatasetAssociation to send to the form + gdas = [] + # Refresh the Dataset to ensure we have a valid set of GroupDatasetAssociations + dataset.dataset.refresh() + for group_dataset_assoc in dataset.dataset.groups: + # Refresh the GroupDatasetAssociation to ensure we have a valid set of permitted_actions + group_dataset_assoc.refresh() + group = galaxy.model.Group.get( group_dataset_assoc.group_id ) + gdas.append( ( group.id, group.name, group_dataset_assoc.permitted_actions ) ) + if "dbkey" in dataset.datatype.metadata_spec and not dataset.metadata.dbkey: + # Copy dbkey into metadata, for backwards compatability + # This looks like it does nothing, but getting the dbkey + # returns the metadata dbkey unless it is None, in which + # case it resorts to the old dbkey. Setting the dbkey + # sets it properly in the metadata + dataset.metadata.dbkey = dataset.dbkey + metadata = list() + # a list of MetadataParemeters + for name, spec in dataset.datatype.metadata_spec.items(): + if spec.visible: + metadata.append( spec.wrap( dataset.metadata.get(name), dataset ) ) + # let's not overwrite the imported datatypes module with the variable datatypes? + ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] + ldatatypes.sort() + return trans.fill_template( "/admin/library/dataset.mako", + dataset=dataset, + metadata=metadata, + datatypes=ldatatypes, + dataset_actions=dataset_actions, + gdas=gdas, + err=None ) + else: + return trans.show_error_message( "Invalid dataset specified" ) diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index 529a705385e..dcb6a0c1be5 100644 --- a/lib/galaxy/web/controllers/async.py +++ b/lib/galaxy/web/controllers/async.py @@ -60,7 +60,7 @@ class ASync( BaseController ): if STATUS == 'OK': key = hmac.new( trans.app.config.tool_secret, "%d:%d" % ( data.id, data.history_id), sha ).hexdigest() if key != data_secret: - return "You do not have permision to alter data %s." % data_id + return "You do not have permission to alter data %s." % data_id # push the job into the queue data.state = data.blurb = data.states.RUNNING log.debug('executing tool %s' % tool.id) @@ -104,6 +104,8 @@ class ASync( BaseController ): #history.datasets.add_dataset( data ) data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE ) + # TODO, Nate: Make sure the following is functionally correct. + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) data.name = GALAXY_NAME data.dbkey = GALAXY_BUILD data.info = GALAXY_INFO diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index f86b648b022..3d43b5bef5e 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -103,28 +103,27 @@ class DatasetInterface( BaseController ): @web.expose def display(self, trans, dataset_id=None, filename=None, **kwd): """Catches the dataset id and displays file contents as directed""" - if filename is None or filename.lower() == "index": - try: - data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) - if data: - mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) - trans.response.set_content_type(mime) - trans.log_event( "Display dataset id: %s" % str(dataset_id) ) - try: - return open( data.file_name ) - except: - return "This item contains no content" - except: - pass - return "Invalid dataset specified" - else: - #display files from directory here - try: - file_path = os.path.join(trans.app.model.HistoryDatasetAssociation.get( dataset_id ).extra_files_path, filename) - mime, encoding = mimetypes.guess_type(file_path) - if mime is None: - mime = trans.app.datatypes_registry.get_mimetype_by_extension(".".split(file_path)[-1]) + data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) + if not data: + raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + if filename is None or filename.lower() == "index": + mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) trans.response.set_content_type(mime) - return open(file_path) - except: - raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % (filename) ) + trans.log_event( "Display dataset id: %s" % str( dataset_id ) ) + try: + return open( data.file_name ) + except: + raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) + else: + file_path = os.path.join( data.extra_files_path, filename ) + mime, encoding = mimetypes.guess_type( file_path ) + if mime is None: + mime = trans.app.datatypes_registry.get_mimetype_by_extension( ".".split( file_path )[-1] ) + trans.response.set_content_type( mime ) + try: + return open( file_path ) + except: + raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) + else: + raise paste.httpexceptions.HTTPForbidden( "You are not permitted to access this dataset." ) diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py new file mode 100644 index 00000000000..43c45a58402 --- /dev/null +++ b/lib/galaxy/web/controllers/library.py @@ -0,0 +1,96 @@ + +from galaxy.web.base.controller import * +import logging + +log = logging.getLogger( __name__ ) + +class Library( BaseController ): + @web.expose + def index( self, trans, library_id=None, import_ids=[], **kwd ): + # Need user to get associated Groups and Datasets + user = trans.get_user() + libraries = [] + if import_ids: + # Used for importing a dataset into a user's history + if not isinstance( import_ids, list ): + import_ids = [import_ids] + history = trans.get_history() + for id in import_ids: + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association() + history.add_dataset( dataset ) + dataset.flush() + history.flush() + return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) + elif library_id: + # Since permitted_actions are kept with the GroupDatasetAssociation, each accessible Library will only + # display the subset of [ it's complete set of ] datasets that the user has permission to access. We + # pass group_ids so this can be handled in the template. + if not user: + group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] + else: + group_ids = [] + for user_group_assoc in user.groups: + group_ids.append( user_group_assoc.group_id ) + library = trans.app.model.Library.get( library_id ) + return trans.fill_template( '/library/library.mako', library=library, group_ids=group_ids ) + if user: + # Only display libraries that contain datasets associated with the user's groups + group_ids = [] + for user_group_assoc in user.groups: + group = trans.app.model.Group.get( user_group_assoc.group_id ) + group_ids.append( group.id ) + libs = trans.app.model.Library.select() + for library in libs: + user_can_access = False + # Check for public datasets in the Library's root folder + for library_folder_dataset_assoc in library.root_folder.datasets: + if user_can_access: + 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: + libraries.append( library ) + user_can_access = True + break + for folder in library.root_folder.folders: + if user_can_access: + break + for library_folder_dataset_assoc in folder.datasets: + if user_can_access: + 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: + libraries.append( library ) + user_can_access = True + break + else: + # Only display libraries that contain datasets associated with the public group + group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] + libs = trans.app.model.Library.select() + for library in libs: + public_library = False + # Check for public datasets in the Library's root folder + for library_folder_dataset_assoc in library.root_folder.datasets: + if public_library: + 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: + libraries.append( library ) + public_library = True + break + # Check for public datasets in the root folder's sub-folders + for folder in library.root_folder.folders: + if public_library: + break + for library_folder_dataset_assoc in folder.datasets: + if public_library: + 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: + libraries.append( library ) + public_library = True + break + return trans.fill_template( '/library/libraries.mako', group_ids=group_ids, libraries=libraries ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 74daa905075..6850b069cb7 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -14,6 +14,8 @@ import urllib log = logging.getLogger( __name__ ) class RootController( BaseController ): + # TODO, Nate: This is where a lot of the new dataset security stuff is managed. + # Make sure it is is functionally correct. @web.expose def default(self, trans, target1=None, target2=None, **kwd): @@ -137,22 +139,25 @@ class RootController( BaseController ): except: return "Dataset id '%s' is invalid" %str( id ) if data: - mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) - trans.response.set_content_type(mime) - if tofile: - fStat = os.stat(data.file_name) - trans.response.headers['Content-Length'] = int(fStat.st_size) - if toext[0:1] != ".": - toext = "." + toext - valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - fname = data.name - fname = ''.join(c in valid_chars and c or '_' for c in fname)[0:150] - trans.response.headers["Content-Disposition"] = "attachment; filename=GalaxyHistoryItem-%s-[%s]%s" % (data.hid, fname, toext) - trans.log_event( "Display dataset id: %s" % str(id) ) - try: - return open( data.file_name ) - except: - return "This dataset contains no content" + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) + trans.response.set_content_type(mime) + if tofile: + fStat = os.stat(data.file_name) + trans.response.headers['Content-Length'] = int(fStat.st_size) + if toext[0:1] != ".": + toext = "." + toext + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = data.name + fname = ''.join(c in valid_chars and c or '_' for c in fname)[0:150] + trans.response.headers["Content-Disposition"] = "attachment; filename=GalaxyHistoryItem-%s-[%s]%s" % (data.hid, fname, toext) + trans.log_event( "Display dataset id: %s" % str(id) ) + try: + return open( data.file_name ) + except: + return "This dataset contains no content" + else: + return "You are not privileged to view this dataset." else: return "No dataset with id '%s'" % str( id ) @@ -164,9 +169,12 @@ class RootController( BaseController ): try: data = self.app.model.HistoryDatasetAssociation.get( parent_id ) if data: - child = data.get_child_by_designation(designation) + child = data.get_child_by_designation( designation ) if child: - return self.display(trans, id=child.id, tofile=tofile, toext=toext) + if trans.app.security_agent.allow_action( trans.user, child.permitted_actions.DATASET_ACCESS, dataset = child ): + return self.display( trans, id=child.id, tofile=tofile, toext=toext ) + else: + return "You are not privileged to access this dataset." except Exception: pass return "A child named %s could not be found for data %s" % ( designation, parent_id ) @@ -176,9 +184,12 @@ class RootController( BaseController ): """Returns a file in a format that can successfully be displayed in display_app""" data = self.app.model.HistoryDatasetAssociation.get( id ) if data: - trans.response.set_content_type(data.get_mime()) - trans.log_event( "Formatted dataset id %s for display at %s" % ( str(id), display_app ) ) - return data.as_display_type(display_app, **kwd) + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + trans.response.set_content_type( data.get_mime() ) + trans.log_event( "Formatted dataset id %s for display at %s" % ( str( id ), display_app ) ) + return data.as_display_type( display_app, **kwd ) + else: + return "You are not privileged to access this dataset." else: return "No data with id=%d" % id @@ -206,69 +217,95 @@ class RootController( BaseController ): data = self.app.model.HistoryDatasetAssociation.get( id ) if data is None: return trans.show_error_message( "Problem retrieving dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) - - p = util.Params(kwd, safe=False) - - if p.change: - # The user clicked the Save button on the 'Change data type' form - trans.app.datatypes_registry.change_datatype( data, p.datatype ) - trans.app.model.flush() - elif p.save: - # The user clicked the Save button on the 'Edit Attributes' form - data.name = p.name - data.info = p.info + if data.history.user is not None and data.history.user != trans.user: + return trans.show_error_message( "This instance of a dataset (%s) in a history does not belong to you." % ( data.id ) ) + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + p = util.Params(kwd, safe=False) - # The following for loop will save all metadata_spec items - for name, spec in data.datatype.metadata_spec.items(): - if spec.get("readonly"): - continue - optional = p.get("is_"+name, None) - if optional and optional == 'true': - # optional element... == 'true' actually means it is NOT checked (and therefore ommitted) - setattr(data.metadata,name,None) - else: - setattr(data.metadata,name,spec.unwrap(p.get(name, None), p)) + if p.change: + # The user clicked the Save button on the 'Change data type' form + trans.app.datatypes_registry.change_datatype( data, p.datatype ) + trans.app.model.flush() + elif p.save: + # The user clicked the Save button on the 'Edit Attributes' form + data.name = p.name + data.info = p.info + + # The following for loop will save all metadata_spec items + for name, spec in data.datatype.metadata_spec.items(): + if spec.get("readonly"): + continue + optional = p.get("is_"+name, None) + if optional and optional == 'true': + # optional element... == 'true' actually means it is NOT checked (and therefore ommitted) + setattr(data.metadata,name,None) + else: + setattr(data.metadata,name,spec.unwrap(p.get(name, None), p)) - data.datatype.after_edit( data ) - trans.app.model.flush() - return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) - elif p.detect: - # The user clicked the Auto-detect button on the 'Edit Attributes' form + data.datatype.after_edit( data ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) + elif p.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + for name, spec in data.datatype.metadata_spec.items(): + # We need to be careful about the attributes we are resetting + if name != 'name' and name != 'info' and name != 'dbkey': + if spec.get( 'default' ): + setattr( data.metadata,name,spec.unwrap( spec.get( 'default' ), spec )) + data.datatype.set_meta( data ) + data.datatype.after_edit( data ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) + elif p.convert_data: + """The user clicked the Convert button on the 'Convert to new format' form""" + target_type = kwd.get("target_type", None) + if target_type: + msg = data.datatype.convert_dataset(trans, data, target_type) + return trans.show_ok_message( msg, refresh_frames=['history'] ) + elif p.change_permission: + """The user clicked the change_permission button on the 'Change permissions' form""" + if not trans.user: + return trans.show_error_message( "You must be logged in if you want to change dataset permitted actions." ) + private_dataset = 'private_dataset' + public_group = trans.app.security_agent.get_public_group() + if private_dataset in kwd and trans.app.security_agent.dataset_has_group( data.dataset.id, public_group.id ): + #check user has permission and then remove public group + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): + trans.app.security_agent.disassociate_components( dataset = data, group = public_group ) + else: + return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) + elif private_dataset not in kwd and not trans.app.security_agent.dataset_has_group( data.dataset.id, public_group.id ): + #check user has permission and then add public group + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): + trans.app.security_agent.associate_components( dataset = data, group = public_group) + else: + return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) + else: + return trans.show_error_message( "You have not specified a valid change of permitted actions." ) + return trans.show_ok_message( 'Permitted actions have been changed.', refresh_frames=['history'] ) + + data.datatype.before_edit( data ) + + if "dbkey" in data.datatype.metadata_spec and not data.metadata.dbkey: + # Copy dbkey into metadata, for backwards compatability + # This looks like it does nothing, but getting the dbkey + # returns the metadata dbkey unless it is None, in which + # case it resorts to the old dbkey. Setting the dbkey + # sets it properly in the metadata + data.metadata.dbkey = data.dbkey + metadata = list() + # a list of MetadataParemeters for name, spec in data.datatype.metadata_spec.items(): - # We need to be careful about the attributes we are resetting - if name != 'name' and name != 'info' and name != 'dbkey': - if spec.get( 'default' ): - setattr( data.metadata,name,spec.unwrap( spec.get( 'default' ), spec )) - data.datatype.set_meta( data ) - data.datatype.after_edit( data ) - trans.app.model.flush() - return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) - elif p.convert_data: - """The user clicked the Convert button on the 'Convert to new format' form""" - target_type = kwd.get("target_type", None) - if target_type: - msg = data.datatype.convert_dataset(trans, data, target_type) - return trans.show_ok_message( msg, refresh_frames=['history'] ) - data.datatype.before_edit( data ) - - if "dbkey" in data.datatype.metadata_spec and not data.metadata.dbkey: - # Copy dbkey into metadata, for backwards compatability - # This looks like it does nothing, but getting the dbkey - # returns the metadata dbkey unless it is None, in which - # case it resorts to the old dbkey. Setting the dbkey - # sets it properly in the metadata - data.metadata.dbkey = data.dbkey - metadata = list() - # a list of MetadataParemeters - for name, spec in data.datatype.metadata_spec.items(): - if spec.visible: - metadata.append( spec.wrap( data.metadata.get(name), data ) ) - # let's not overwrite the imported datatypes module with the variable datatypes? - ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] - ldatatypes.sort() - trans.log_event( "Opened edit view on dataset %s" % str(id) ) - return trans.fill_template( "/dataset/edit_attributes.mako", data=data, metadata=metadata, - datatypes=ldatatypes, err=None ) + if spec.visible: + metadata.append( spec.wrap( data.metadata.get(name), data ) ) + # let's not overwrite the imported datatypes module with the variable datatypes? + ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] + ldatatypes.sort() + trans.log_event( "Opened edit view on dataset %s" % str(id) ) + return trans.fill_template( "/dataset/edit_attributes.mako", data=data, metadata=metadata, + datatypes=ldatatypes, err=None ) + else: + return trans.show_error_message( "You do not have permission to edit this dataset's (%s) attributes." % id ) @web.expose def delete( self, trans, id = None, **kwd): @@ -327,6 +364,8 @@ class RootController( BaseController ): self.app.job_stop_queue.put( data.creating_job_associations[0].job ) except IndexError: pass # upload tool will cause this since it doesn't have a job + else: + return "Dataset id '%s' is invalid" %str( id ) return "OK" ## ---- History management ----------------------------------------------- @@ -356,8 +395,13 @@ class RootController( BaseController ): if history: if history.user_id != None and user: assert user.id == history.user_id, "History does not belong to current user" - history_names.append(history.name) + # Delete DefaultHistoryGroupAssociations + for default_history_group_association in history.default_groups: + default_history_group_association.delete() + default_history_group_association.flush() + # Mark history as deleted in db history.deleted = True + history_names.append(history.name) # If deleting the current history, make a new current. if history == trans.get_history(): trans.new_history() @@ -404,7 +448,7 @@ class RootController( BaseController ): send_to_err = "You can't send histories to yourself" else: for history in histories: - new_history = history.copy() + new_history = history.copy( target_user=send_to_user ) new_history.name = history.name+" from "+user.email new_history.user_id = send_to_user.id trans.log_event( "History share, id: %s, name: '%s': to new id: %s" % (str(history.id), history.name, str(new_history.id)) ) @@ -441,7 +485,7 @@ class RootController( BaseController ): if user: if import_history.user_id == user.id: return trans.show_error_message( "You cannot import your own history.") - new_history = import_history.copy() + new_history = import_history.copy( target_user=trans.user ) new_history.name = "imported: "+new_history.name new_history.user_id = user.id galaxy_session = trans.get_galaxy_session() @@ -548,11 +592,16 @@ class RootController( BaseController ): return trans.show_message( "%s" % change_msg, refresh_frames=['history'] ) @web.expose - def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",**kwd ): + def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",copy_access_from=None,**kwd ): """Adds a POSTed file to a History""" try: history = trans.app.model.History.get( history_id ) + groups = history.default_groups + if copy_access_from: + copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) + groups = copy_access_from.dataset.groups data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True ) + trans.app.security_agent.set_dataset_groups( data.dataset, groups ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -569,9 +618,45 @@ class RootController( BaseController ): data.flush() trans.log_event("Added dataset %d to history %d" %(data.id, trans.history.id)) return trans.show_ok_message("Dataset "+str(data.hid)+" added to history "+str(history_id)+".") - except: + except Exception, e: + trans.log_event( "Failed to add dataset to history: %s" % ( e ) ) return trans.show_error_message("Adding File to History has Failed") + @web.expose + def history_set_default_permitted_actions( self, trans, **kwd ): + """Sets the user's default permitted_actions for the current history""" + if trans.user: + if 'set_permitted_actions' in kwd: + """The user clicked the set_permitted_actions button on the set_permitted_actions form""" + history = trans.get_history() + group_in = [] + group_out = [] + # Collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.security_agent.get_group( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in history.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + trans.app.security_agent.history_set_default_access( history, groups=group_in ) + return trans.show_ok_message( 'Default history permitted actions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to this history's default permitted actions." ) + return trans.fill_template( 'history/permissions.mako' ) + else: + #user not logged in, history group must be only public + return trans.show_error_message( "You must be logged in to change a history's default permitted actions." ) + + @web.expose def dataset_make_primary( self, trans, id=None): """Copies a dataset and makes primary""" @@ -596,8 +681,15 @@ class RootController( BaseController ): bugs_email = trans.app.config.get( "bugs_email", "mailto:galaxy-bugs@bx.psu.edu" ) blog_url = trans.app.config.get( "blog_url", "http://g2.trac.bx.psu.edu/blog" ) screencasts_url = trans.app.config.get( "screencasts_url", "http://g2.trac.bx.psu.edu/wiki/ScreenCasts" ) + admin_user = "false" + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + user = trans.get_user() + if user: + user_email = trans.get_user().email + if user_email in admin_users: + admin_user = "true" return trans.fill_template( "/root/masthead.mako", brand=brand, wiki_url=wiki_url, - blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url ) + blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url, admin_user=admin_user ) @web.expose def dataset_errors( self, trans, id=None, **kwd ): diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 41890611abc..d46abb922c5 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -118,6 +118,7 @@ class User( BaseController ): user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) user.flush() + trans.app.security_agent.setup_new_user( user ) trans.set_user( user ) trans.ensure_valid_galaxy_session() """ @@ -166,3 +167,37 @@ class User( BaseController ): return trans.show_form( web.FormBuilder( web.url_for(), "Reset Password", submit_text="Submit" ) .add_text( "email", "Email", value=email, error=error ) ) + + @web.expose + def set_default_permitted_actions( self, trans, **kwd ): + # TODO, Nate: Make sure this method is functionally correct. + """Sets the user's default permitted actions for the new histories""" + if trans.user: + if 'set_permitted_actions' in kwd: + """The user clicked the set_permitted_actions button on the set_permitted_actions form""" + group_in = [] + group_out = [] + # Collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.security_agent.get_group( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in trans.user.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + trans.app.security_agent.user_set_default_access( trans.user, groups = group_in ) + return trans.show_ok_message( 'Default new history permitted actions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to new history's default permitted actions." ) + return trans.fill_template( 'user/permissions.mako' ) + else: + # User not logged in, history group must be only public + return trans.show_error_message( "You must be logged in to change your default permitted actions." ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 688e4ae1240..87f099a681d 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -197,9 +197,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): return self.new_history() return self.__history def new_history( self ): - history = self.app.model.History() + history = self.app.model.History( user = self.user ) # Make sure we have an id history.flush() + self.app.security_agent.history_set_default_access( history ) # Immediately associate the new history with self self.__history = history # Make sure we have a valid session to associate with the new history @@ -271,6 +272,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): user.set_password_cleartext( 'external' ) user.external = True user.flush() + self.app.security_agent.setup_new_user( user ) self.log_event( "Automatically created account '%s'" % user.email ) return user def get_cookie_user( self ): @@ -431,6 +433,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.flush() self.__galaxy_session = galaxy_session if history is not None and user is not None: + # TODO, Nate: Make sure the following is functionally correct + if not history.user: + # This user will now acquire previously non-owned history, so set permitted actions to user's default + self.app.security_agent.history_set_default_access( history, groups=user.default_groups, dataset=True ) history.user_id = user.id history.flush() self.__history = history @@ -522,8 +528,8 @@ class FormBuilder( object ): self.action = action self.submit_text = submit_text self.inputs = [] - def add_input( self, type, name, label, value=None, error=None, help=None ): - self.inputs.append( FormInput( type, label, name, value, error, help ) ) + def add_input( self, type, name, label, value=None, error=None, help=None, use_label=True ): + self.inputs.append( FormInput( type, label, name, value, error, help, use_label ) ) return self def add_text( self, name, label, value=None, error=None, help=None ): return self.add_input( 'text', label, name, value, error, help ) @@ -534,13 +540,14 @@ class FormInput( object ): """ Simple class describing a form input element """ - def __init__( self, type, name, label, value=None, error=None, help=None ): + def __init__( self, type, name, label, value=None, error=None, help=None, use_label=True ): self.type = type self.name = name self.label = label self.value = value self.error = error self.help = help + self.use_label = use_label class FormData( object ): """ diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako new file mode 100644 index 00000000000..28596e03fb3 --- /dev/null +++ b/templates/admin/dataset_security/group_create.mako @@ -0,0 +1,96 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Create Group%def> + + + Libraries | + Groups | + Users + + Create Group + + %if msg: + ${msg} + %endif + + + + + + Name: Priority: + + %if len( users ) == 0: + There are no Galaxy users + %else: + Add Members to Group - Quick Find + + + |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z + + + + + <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% email = unescape( user[1], unentities ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: + + %else: + + %endif + + %if email.upper().startswith( curr_anchor ): + %if not anchored: + + <% anchored = True %> + %endif + ${email} + %else: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: + + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif + + <% ctr += 1 %> + %endfor + + + %endif + Create + + + + + + diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako new file mode 100644 index 00000000000..58406d7edbf --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,71 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Permitted Actions on Datasets%def> + + + Libraries | + Groups | + Users + + Manage Permitted Actions on Datasets for Group '${gn}' + + %if msg: + ${msg} + %endif + + %if len( gdas ) == 0: + There is no Galaxy group named '${gn}' + %else: + + Group + Priority + Permitted Actions on Datasets + + <% ctr = 0 %> + + %for gda in gdas: + %if ctr % 2 == 1: + + %else: + + %endif + ${gn} + ${gda[0]} + + %for da in dataset_actions: + <% check = False %> + %for action in gda[1]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da} + %endfor + + + + <% ctr += 1 %> + %endfor + Update + + %endif + + diff --git a/templates/admin/dataset_security/group_members.mako b/templates/admin/dataset_security/group_members.mako new file mode 100644 index 00000000000..36e407959f6 --- /dev/null +++ b/templates/admin/dataset_security/group_members.mako @@ -0,0 +1,47 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Create Group%def> + + + Libraries | + Groups | + Users + + + Manage group membership + + + Members of Group '${gn}' + + %if msg: + ${msg} + %endif + + %if len( members ) == 0: + Group '${gn}' contains no members + %else: + <% ctr = 0 %> + %for member in members: + <% email = unescape( member[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + ${email} + + <% ctr += 1 %> + %endfor + %endif + + diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako new file mode 100644 index 00000000000..c7defaa7697 --- /dev/null +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -0,0 +1,114 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Manage Group Membership%def> + + + Libraries | + Groups | + Users + + + %if msg: + ${msg} + %endif + <% gn = unescape( group_name, unentities ) %> + + + + + + %if len( users ) == 0: + There are no Galaxy users + %else: + 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: + + %else: + + %endif + + %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 + + <% ctr += 1 %> + %endfor + + + %endif + Update Membership + + + + + + diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako new file mode 100644 index 00000000000..0687802932e --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,67 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Groups%def> + + + Libraries | + Users + + + Create a new group + + Manage deleted groups + + + Groups + + %if msg: + ${msg} + %endif + %if len( groups ) == 0: + There are no Galaxy groups + %else: + + Group + Priority + Members + Datasets + Group Permitted Actions on Datasets + + + <% ctr = 0 %> + %for group in groups: + <% group_name = unescape( group[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + ${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 + + <% ctr += 1 %> + %endfor + %endif + + 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%def> + + Dataset Security + + %if msg: + ${msg} + %endif + Groups + Users + + diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako new file mode 100644 index 00000000000..8a392a67e31 --- /dev/null +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -0,0 +1,56 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% email = unescape( user_email, unentities ) %> + +<%def name="title()">Create Group%def> + + + Libraries | + Groups | + Users + + Groups of which '${email}' is a member + + %if msg: + ${msg} + %endif + %if len( groups ) == 0: + User '${email}' belongs to no groups + %else: + + Group + Priority + Datasets + Permitted Actions on Datasets + + <% ctr = 0 %> + %for group in groups: + <% gn = unescape( group[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + ${gn} + ${group[2]} + %if group[3] > 0: + ${group[3]} + %else: + ${group[3]} + %endif + + %for da in group[4]: + ${da} + %endfor + + + <% ctr += 1 %> + %endfor + %endif + + diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..22abae62d54 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,80 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Users%def> + + + Groups | + Libraries + + + %if msg: + ${msg} + %endif + + %if len( users ) == 0: + There are no Galaxy users + %else: + 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 + + + <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% email = unescape( user[1], unentities ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: + + %else: + + %endif + + %if email.upper().startswith( curr_anchor ): + %if not anchored: + + <% anchored = True %> + %endif + ${email} + %else: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: + + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif + + <% ctr += 1 %> + %endfor + + %endif + + diff --git a/templates/admin/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: + ${msg} + %endif + 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> + +<%def name="datatype( dataset, datatypes )"> + + ## $datatypes.sort() + %for ext in datatypes: + %if dataset.ext == ext: + ${ext} + %else: + ${ext} + %endif + %endfor + +%def> + +<%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 +%def> + + + Group Associations + + + + %for gda in gdas: + Group:${gda[1]} + Permitted actions on dataset: + + ${group_dataset_permitted_actions( dataset_actions, gda )} + + %endfor + + + + + + Edit Attributes + + + + + Name: + + + + + + + Info: + + + + + + %for element in metadata: + + ${element.spec.desc}: + + ${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 + + + + + New Type: + + ${datatype( dataset, datatypes )} + + + This will change the datatype of the existing dataset + but not modify its contents. Use this if Galaxy + has incorrectly guessed the type of your dataset. + + + + + + + + + +manage containing folder + diff --git a/templates/admin/library/folder.mako b/templates/admin/library/folder.mako new file mode 100644 index 00000000000..e93b64e7989 --- /dev/null +++ b/templates/admin/library/folder.mako @@ -0,0 +1,158 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + return render_dataset( component ) + %> +%def> +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + + ${data.display_name()} + + + ## Header row for history items (name, state, action buttons) + + %if data_state != 'ok': + + %endif + + + + + ##${data.display_name()} + + ## Body for history items, extra info and actions, data "peek" + + %if data_state == "queued": + Job is waiting to run + %elif data_state == "running": + Job is currently running + %elif data_state == "error": + + An error occurred running this job: ${data.display_info().strip()}, + report this error + + %elif data_state == "empty": + No data: ${data.display_info()} + %elif data_state == "ok": + + ${data.blurb}, + format: ${data.ext}, + database: + %if data.dbkey == '?': + ${data.dbkey} + %else: + ${data.dbkey} + %endif + + Info: ${data.display_info()} + %if data.peek != "no peek": + ${data.display_peek()} + %endif + %else: + Error: unknown dataset state "${data_state}". + %endif + ## Recurse for child datasets + + + +%def> +## Render a folder +<%def name="render_folder( this_folder )"> + + Contents of Folder: ${this_folder.name} + + + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> + %for component in components: + ${render_component( component )} + %endfor + + + + Add new dataset to folder '${this_folder.name}' + + Add new folder to folder '${this_folder.name}' + + + + +%def> +<%def name="title()">Manage Folder: ${folder.name}%def> + + + Libraries | + Groups | + Users + + Change Folder Attributes + + + + Name: + + + + + + + Description: + + + + + + + + + + + + + + + + + + + + Manage Folder Contents: ${folder.name} + + + %if folder.parent: + Up a Level + %elif folder.library_root: + Manage Library + %endif + + + + ${render_folder( folder )} + + + + diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako new file mode 100644 index 00000000000..7e44442e1aa --- /dev/null +++ b/templates/admin/library/libraries.mako @@ -0,0 +1,19 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Libraries%def> + + + Groups | + Users + + Create a new library + + Galaxy Libraries + + %for library in libraries: + + ${library.name} + + %endfor + + diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako new file mode 100644 index 00000000000..5d979af8abb --- /dev/null +++ b/templates/admin/library/library.mako @@ -0,0 +1,35 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Library%def> + + + Libraries | + Groups | + Users + + Manage Library '${library.name}' + + + + + Name: + + + + + + + Description: + + + + + + + + + + + + Manage Root Folder + diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako new file mode 100644 index 00000000000..3c641288b74 --- /dev/null +++ b/templates/admin/library/new_dataset.mako @@ -0,0 +1,82 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Create New Library Dataset%def> + + + Libraries | + Groups | + Users + + Create a new Library Dataset + + + + + File: + + + + + URL/Text: + + + Here you may specify a list of URLs (one per line) or paste the contents of a file. + + + + + Convert spaces to tabs: + Yes + + Use this option if you are entering intervals by hand. + + + + + File Format: + + + Auto-detect + %for file_format in file_formats: + ${file_format} + %endfor + + + + Which format? See help below + + + + + Genome: + + + %for dbkey in dbkeys: + %if dbkey[1] == last_used_build: + ${dbkey[0]} + %else: + ${dbkey[0]} + %endif + %endfor + + + + + + Associate with Groups: + Multi-select list - hold the appropriate key while clicking to select multiple columns + + + %for group in groups: + ${group[1]} + %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: + ${msg} + %endif + + + + + Reload tool: + + %for i, section in enumerate( toolbox.sections ): + + %for t in section.tools: + ${t.name} + %endfor + %endfor + + Reload + + + + + + 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%def> - - - - - Galaxy Administration - %if msg: - ${msg} - %endif - - - - - - Admin password: - - Reload tool: - - %for i, section in enumerate( toolbox.sections ): - - %for t in section.tools: - ${t.name} - %endfor - %endfor - - Reload - - - - - - diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index faf11771489..c54f6eb0d1a 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -1,5 +1,5 @@ <%inherit file="/base.mako"/> -<%def name="title()">Your saved histories%def> +<%def name="title()">Edit Dataset Attributes%def> <%def name="datatype( dataset, datatypes )"> @@ -130,3 +130,36 @@ + + + +%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ): + + Change Permitted Actions + + + + + + Private Dataset: + + <% checked = "" %> + %if not trans.app.security_agent.dataset_has_group( data.id, trans.app.model.Group.get_public_group().id ): + <% checked = " checked" %> + %endif + + + + + + This will prevent other users from viewing or utilizing this dataset, even if you share your history with them. + + + + + + + + + +%endif diff --git a/templates/form.mako b/templates/form.mako index c003de02585..42fdb745ba3 100644 --- a/templates/form.mako +++ b/templates/form.mako @@ -21,9 +21,11 @@ $(function(){ cls += " form-row-error" %> + %if input.use_label: ${input.label}: + %endif diff --git a/templates/history/options.mako b/templates/history/options.mako index 4faa967742d..4bebc932dca 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,6 +18,7 @@ %endif %if app.config.enable_beta_features: Construct workflow from the current history + Change default permitted actions for the current history %endif Share current history %endif diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako new file mode 100644 index 00000000000..6607f002c41 --- /dev/null +++ b/templates/history/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permitted Actions%def> + +%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 ] %> + + + GroupInOut + %for group in user_groups: + ${group.name} + %endfor + + + + + + + This will change the default permitted actions assigned to new datasets for your current history. + + + + + + + + + +%endif \ No newline at end of file diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako new file mode 100644 index 00000000000..1176bd2162a --- /dev/null +++ b/templates/library/libraries.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Libraries You Can Access%def> + + Libraries You Can Access + + %for library in libraries: + + ${library.name} + + %endfor + + diff --git a/templates/library/library.mako b/templates/library/library.mako new file mode 100644 index 00000000000..373ff9c4118 --- /dev/null +++ b/templates/library/library.mako @@ -0,0 +1,69 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + render = False + # Check the folder's datasets to see what can be rendered + for library_folder_dataset_assoc in component.datasets: + if render: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + # TODO: Do we need to upgrade sqlalchemy? The following shouldn't be necessary if the mappers work correctly. + # Check the folder's sub-folders to see what can be rendered + for library_folder in component.folders: + render_component( library_folder ) + if render: + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + render = False + dataset = trans.app.model.Dataset.get( component.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + if render: + return render_dataset( component ) + %> +%def> + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> + + ${data.name} + +%def> + +## Render a folder +<%def name="render_folder( this_folder )"> + + Folder: ${this_folder.name} + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> + + %for component in components: + ${render_component( component )} + %endfor + + +%def> + +<%def name="title()">View Library: ${library.name}%def> + + 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 @@ -%def> \ No newline at end of file +%def> diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 58d8f1776db..097d7707acc 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -20,6 +20,9 @@ | wiki | screencasts | blog + %if admin_user == "true": + | admin + %endif diff --git a/templates/user/index.mako b/templates/user/index.mako index 2b11262cece..1047466aad0 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -8,6 +8,9 @@ Change your password Update your email address + %if app.config.enable_beta_features: + Change default permitted actions for new histories + %endif Logout %else: diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako new file mode 100644 index 00000000000..7b6b90f976d --- /dev/null +++ b/templates/user/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permitted Actions%def> + +%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 ] %> + + + GroupInOut + %for group in user_groups: + ${group.name} + %endfor + + + + + + + This will change the default permitted actions assigned to new datasets for new histories. + + + + + + + + + +%endif \ No newline at end of file diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index e46f0c54ad1..69ccfe583ee 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -12,6 +12,7 @@ + diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml new file mode 100644 index 00000000000..e6dabfbf1b1 --- /dev/null +++ b/tools/data_source/access_libraries.xml @@ -0,0 +1,7 @@ + + + stored locally + + + + \ No newline at end of file diff --git a/tools/data_source/encode_import_code.py b/tools/data_source/encode_import_code.py index 09a6e8a9823..ddfbb0241e0 100644 --- a/tools/data_source/encode_import_code.py +++ b/tools/data_source/encode_import_code.py @@ -5,7 +5,8 @@ from shutil import copyfile #post processing, set build for data and add additional data to history def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr): - history = out_data.items()[0][1].history + base_dataset = out_data.items()[0][1] + history = base_dataset.history if history == None: print "unknown history!" return @@ -37,6 +38,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + description + ")" history.add_dataset( newdata ) + #TODO, Nate: Make sure the following is functionally correct + app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) app.model.flush() try: copyfile(filepath,newdata.file_name) diff --git a/tools/data_source/microbial_import_code.py b/tools/data_source/microbial_import_code.py index b6bc2f6bd85..e8816f093ff 100644 --- a/tools/data_source/microbial_import_code.py +++ b/tools/data_source/microbial_import_code.py @@ -84,7 +84,8 @@ from galaxy import datatypes, config, jobs from shutil import copyfile def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr): - history = out_data.items()[0][1].history + base_dataset = out_data.items()[0][1] + history = base_dataset.history if history == None: print "unknown history!" return @@ -128,6 +129,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + microbe_info[kingdom][org]['chrs'][chr]['data'][description]['feature'] +" for "+microbe_info[kingdom][org]['name']+":"+chr + ")" newdata.flush() + #TODO, Nate: Make sure the following is functionally correct + app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) history.add_dataset( newdata ) app.model.flush() try: diff --git a/tools/maf/maf_to_bed_code.py b/tools/maf/maf_to_bed_code.py index c8f1e905e8e..428486b3e37 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -27,12 +27,13 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr fields = line.split("\t") dbkey = fields[1] filepath = fields[2] - newdata = app.model.HistoryDatasetAssociation( create_dataset = True ) newdata.extension = "bed" newdata.name = basic_name + " (" + dbkey + ")" newdata.flush() history.add_dataset( newdata ) + #TODO, Nate: Make sure the following is functionally correct + app.security_agent.set_dataset_groups( newdata.dataset, output_data.dataset.groups ) newdata.flush() history.flush() app.model.flush() diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 6dcdf061f94..58ca578eb15 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -77,8 +77,8 @@ use_lint = false # NEVER enable this on a public site (even test or QA) use_interactive = true -# Admin Password -admin_pass = galaxy +# Admin Users - this should be a comma-separated list of valid Galaxy users +#admin_users = user1@bx.psu.edu,user2@bx.psu.edu # path to sendmail sendmail_path = /usr/sbin/sendmail
%s" % change_msg, refresh_frames=['history'] ) @web.expose - def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",**kwd ): + def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",copy_access_from=None,**kwd ): """Adds a POSTed file to a History""" try: history = trans.app.model.History.get( history_id ) + groups = history.default_groups + if copy_access_from: + copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) + groups = copy_access_from.dataset.groups data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True ) + trans.app.security_agent.set_dataset_groups( data.dataset, groups ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -569,9 +618,45 @@ class RootController( BaseController ): data.flush() trans.log_event("Added dataset %d to history %d" %(data.id, trans.history.id)) return trans.show_ok_message("Dataset "+str(data.hid)+" added to history "+str(history_id)+".") - except: + except Exception, e: + trans.log_event( "Failed to add dataset to history: %s" % ( e ) ) return trans.show_error_message("Adding File to History has Failed") + @web.expose + def history_set_default_permitted_actions( self, trans, **kwd ): + """Sets the user's default permitted_actions for the current history""" + if trans.user: + if 'set_permitted_actions' in kwd: + """The user clicked the set_permitted_actions button on the set_permitted_actions form""" + history = trans.get_history() + group_in = [] + group_out = [] + # Collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.security_agent.get_group( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in history.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + trans.app.security_agent.history_set_default_access( history, groups=group_in ) + return trans.show_ok_message( 'Default history permitted actions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to this history's default permitted actions." ) + return trans.fill_template( 'history/permissions.mako' ) + else: + #user not logged in, history group must be only public + return trans.show_error_message( "You must be logged in to change a history's default permitted actions." ) + + @web.expose def dataset_make_primary( self, trans, id=None): """Copies a dataset and makes primary""" @@ -596,8 +681,15 @@ class RootController( BaseController ): bugs_email = trans.app.config.get( "bugs_email", "mailto:galaxy-bugs@bx.psu.edu" ) blog_url = trans.app.config.get( "blog_url", "http://g2.trac.bx.psu.edu/blog" ) screencasts_url = trans.app.config.get( "screencasts_url", "http://g2.trac.bx.psu.edu/wiki/ScreenCasts" ) + admin_user = "false" + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + user = trans.get_user() + if user: + user_email = trans.get_user().email + if user_email in admin_users: + admin_user = "true" return trans.fill_template( "/root/masthead.mako", brand=brand, wiki_url=wiki_url, - blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url ) + blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url, admin_user=admin_user ) @web.expose def dataset_errors( self, trans, id=None, **kwd ): diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 41890611abc..d46abb922c5 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -118,6 +118,7 @@ class User( BaseController ): user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) user.flush() + trans.app.security_agent.setup_new_user( user ) trans.set_user( user ) trans.ensure_valid_galaxy_session() """ @@ -166,3 +167,37 @@ class User( BaseController ): return trans.show_form( web.FormBuilder( web.url_for(), "Reset Password", submit_text="Submit" ) .add_text( "email", "Email", value=email, error=error ) ) + + @web.expose + def set_default_permitted_actions( self, trans, **kwd ): + # TODO, Nate: Make sure this method is functionally correct. + """Sets the user's default permitted actions for the new histories""" + if trans.user: + if 'set_permitted_actions' in kwd: + """The user clicked the set_permitted_actions button on the set_permitted_actions form""" + group_in = [] + group_out = [] + # Collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.security_agent.get_group( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in trans.user.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + trans.app.security_agent.user_set_default_access( trans.user, groups = group_in ) + return trans.show_ok_message( 'Default new history permitted actions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to new history's default permitted actions." ) + return trans.fill_template( 'user/permissions.mako' ) + else: + # User not logged in, history group must be only public + return trans.show_error_message( "You must be logged in to change your default permitted actions." ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 688e4ae1240..87f099a681d 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -197,9 +197,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): return self.new_history() return self.__history def new_history( self ): - history = self.app.model.History() + history = self.app.model.History( user = self.user ) # Make sure we have an id history.flush() + self.app.security_agent.history_set_default_access( history ) # Immediately associate the new history with self self.__history = history # Make sure we have a valid session to associate with the new history @@ -271,6 +272,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): user.set_password_cleartext( 'external' ) user.external = True user.flush() + self.app.security_agent.setup_new_user( user ) self.log_event( "Automatically created account '%s'" % user.email ) return user def get_cookie_user( self ): @@ -431,6 +433,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.flush() self.__galaxy_session = galaxy_session if history is not None and user is not None: + # TODO, Nate: Make sure the following is functionally correct + if not history.user: + # This user will now acquire previously non-owned history, so set permitted actions to user's default + self.app.security_agent.history_set_default_access( history, groups=user.default_groups, dataset=True ) history.user_id = user.id history.flush() self.__history = history @@ -522,8 +528,8 @@ class FormBuilder( object ): self.action = action self.submit_text = submit_text self.inputs = [] - def add_input( self, type, name, label, value=None, error=None, help=None ): - self.inputs.append( FormInput( type, label, name, value, error, help ) ) + def add_input( self, type, name, label, value=None, error=None, help=None, use_label=True ): + self.inputs.append( FormInput( type, label, name, value, error, help, use_label ) ) return self def add_text( self, name, label, value=None, error=None, help=None ): return self.add_input( 'text', label, name, value, error, help ) @@ -534,13 +540,14 @@ class FormInput( object ): """ Simple class describing a form input element """ - def __init__( self, type, name, label, value=None, error=None, help=None ): + def __init__( self, type, name, label, value=None, error=None, help=None, use_label=True ): self.type = type self.name = name self.label = label self.value = value self.error = error self.help = help + self.use_label = use_label class FormData( object ): """ diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako new file mode 100644 index 00000000000..28596e03fb3 --- /dev/null +++ b/templates/admin/dataset_security/group_create.mako @@ -0,0 +1,96 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Create Group%def> +
${msg}
${data.display_peek()}
+ Reload tool: + + %for i, section in enumerate( toolbox.sections ): + + %for t in section.tools: + ${t.name} + %endfor + %endfor + + Reload +
Admin password:
- Reload tool: - - %for i, section in enumerate( toolbox.sections ): - - %for t in section.tools: - ${t.name} - %endfor - %endfor - - Reload -
+ %for component in components: + ${render_component( component )} + %endfor +