From fddd3b572fbfdfb3d4d4b6c9f159f7687364a730 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:44:10 -0400 Subject: [PATCH 01/94] RECOMMIT: First pass with adding role based access controls. Added roles and groups. Users can be associated with groups. Roles can be associated with roles, datasets, groups, and users. Currently the creator of a dataset can mark the dataset as private, preventing other users from viewing or utilizing this dataset, even if it's containing history is shared. This is done via the edit attributes page for a particular dataset. This requires enable_beta_features to be set. --- lib/galaxy/datatypes/images.py | 4 +- lib/galaxy/model/__init__.py | 574 ++++++++++++++++++++----- lib/galaxy/model/mapping.py | 170 ++++++++ lib/galaxy/tools/__init__.py | 4 +- lib/galaxy/tools/actions/__init__.py | 16 +- lib/galaxy/tools/actions/upload.py | 4 +- lib/galaxy/tools/parameters/basic.py | 24 +- lib/galaxy/web/controllers/async.py | 2 +- lib/galaxy/web/controllers/dataset.py | 47 +- lib/galaxy/web/controllers/root.py | 220 ++++++---- lib/galaxy/web/framework/__init__.py | 5 +- templates/dataset/edit_attributes.mako | 35 +- templates/root/history_common.mako | 4 +- 13 files changed, 875 insertions(+), 234 deletions(-) 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..0bed6bb7d19 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -27,19 +27,99 @@ def set_datatypes_registry( d_registry ): datatypes_registry = d_registry class User( object ): - def __init__( self, email=None, password=None ): + def __init__( self, email=None, password=None, groups = [], roles = [], default_groups = [], default_roles = [] ): self.email = email self.password = password self.external = False # Relationships self.histories = [] + if not groups: + groups.append( GalaxyGroup.get( GalaxyGroup.public_id ) ) + default_groups.append( groups[-1] ) + default_groups.append( self.create_private_group() ) + group_id_added = [] + for group in groups: + if group.id not in group_id_added: + group.add_user( self ) + group_id_added.append( group.id ) + group_id_added = [] + for group in default_groups: + if group.id not in group_id_added: + user_group_assoc = DefaultUserGroupAssociation( self, group ) + user_group_assoc.flush() + group_id_added.append( group.id ) + role_id_added = [] + for role in roles: + if role.id not in role_id_added: + role.add_user( self ) + role_id_added.append( role.id ) + role_id_added = [] + for role in default_roles: + if role.id not in role_id_added: + role_group_assoc = DefaultUserRoleAssociation( self, role ) + role_group_assoc.flush() + role_id_added.append( role.id ) 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() + def create_private_group( self ): + #create private group + group = GalaxyGroup( self.email, priority = 10 ) + group.flush() + #create private dataset access role + role = AccessRole( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ), priority = 1 ) + role.flush() + #add role to group + group.add_role( role ) + #create roles for user modification of role + user_role = AccessRole( "%s role modification" % self.email, list( AccessRole.access_actions.__dict__.values() ) ) + user_role.flush() + #add role to user + user_role.add_user( self ) + #add role to role + role.add_role( user_role ) + + #create roles for user modification of group + group_role = AccessRole( "%s group modification" % self.email, list( GalaxyGroup.access_actions.__dict__.values() ) ) + group_role.flush() + #add role to group + group.add_access_role( group_role ) + #associate role and user + group_role.add_user( self ) + + #add user to group + group.add_user( self ) + group.flush() + return group + def add_group( self, group ): + return group.add_user( self ) + def has_group( self, check_group ): + return bool( UserGroupAssociation.get_by( group_id = check_group.id, user_id = self.id ) ) + def has_role( self, check_role ): + return bool( UserRoleAssociation.get_by( role_id = check_role.id, user_id = self.id ) ) + def set_default_access( self, groups = None, roles = None, history = False, dataset = False ): + if groups is not None: + for assoc in self.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + assoc = DefaultUserGroupAssociation( self, group ) + assoc.flush() + if roles is not None: + for assoc in self.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = DefaultUserRoleAssociation( self, role ) + assoc.flush() + if history: + for history in self.histories: + history.set_default_access( groups = groups, roles = roles, dataset = dataset ) + class Job( object ): """ A job represents a request to run a tool given input datasets, tool @@ -101,10 +181,338 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset +class AccessRole( object ): + dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading + USE = 'dataset_use', #use in jobs + ADD_ROLE = 'dataset_add_role', #dataset can be added to roles + REMOVE_ROLE = 'dataset_remove_role', #dataset can be removed from roles + ADD_GROUP = 'dataset_add_group', #dataset can be added to groups + REMOVE_GROUP = 'dataset_remove_group' ) #dataset can be removed from groups + role_actions = Bunch( ADD_DATASET = 'role_add_dataset', #add role to dataset + REMOVE_DATASET = 'role_remove_dataset', #remove role from dataset + DELETE = 'role_delete', #delete a role + MODIFY = 'role_modify', #change a role's actions, + ADD_GROUP = 'role_add_group', #add role to a group + REMOVE_GROUP = 'role_remove_group' ) #remove role from a group + group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add group to dataset + REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group + DELETE = 'group_delete', #delete a group + ADD_ROLE = 'group_add_role', #add role to group + REMOVE_ROLE = 'group_remove_role', #remove role from group + ADD_USER = 'group_add_user' ) #add users to group + + access_actions = role_actions + + def __init__( self, name, actions, priority = 0 ): + self.name = name + if not isinstance( actions, list ): + actions = [ actions ] + self.actions = actions + self.priority = priority + def add_user( self, user ): + assoc = UserRoleAssociation( user, self ) + assoc.flush() + return assoc + def add_group( self, group ): + assoc = GroupRoleAssociation( group, self ) + assoc.flush() + return assoc + def add_role( self, role ): + assoc = RoleRoleAssociation( role, self ) + assoc.flush() + return assoc + def add_dataset( self, dataset ): + assoc = RoleDatasetAssociation( self, dataset ) + assoc.flush() + return assoc + +class GalaxyGroup( object ): + public_id = None + access_actions = AccessRole.group_actions + def __init__( self, name, priority = 0 ): + self.name = name + self.priority = priority + def add_user( self, user ): + assoc = UserGroupAssociation( user, self ) + assoc.flush() + return assoc + def add_role( self, role ): + return role.add_group( self ) + def add_access_role( self, role ): + assoc = GroupRoleAccessAssociation( self, role ) + assoc.flush() + return assoc + def add_dataset( self, dataset ): + assoc = GroupDatasetAssociation( self, dataset ) + assoc.flush() + return assoc + +class UserGroupAssociation( object ): + def __init__( self, user, group ): + self.user = user + self.group = group + +class RoleRoleAssociation( object ): + def __init__( self, role, target_role ): + self.role = role + self.target_role = target_role + +class GroupRoleAccessAssociation( object ): + def __init__( self, group, role ): + self.group = group + self.role = role + +class GroupRoleAssociation( object ): + def __init__( self, group, role ): + self.group = group + self.role = role + +class UserRoleAssociation( object ): + def __init__( self, user, role ): + self.user = user + self.role = role + +class GroupDatasetAssociation( object ): + def __init__( self, group, dataset ): + 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 + +class RoleDatasetAssociation( object ): + def __init__( self, role, dataset ): + if isinstance( role, RoleDatasetAssociation ) or isinstance( role, DefaultUserRoleAssociation ) or isinstance( role, DefaultHistoryRoleAssociation ): + role = role.role + self.role = role + + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset + self.dataset = dataset + +class DefaultUserRoleAssociation( object ): + def __init__( self, user, role ): + if isinstance( role, RoleDatasetAssociation ) or isinstance( role, DefaultUserRoleAssociation ) or isinstance( role, DefaultHistoryRoleAssociation ): + role = role.role + self.user = user + self.role = role + +class DefaultUserGroupAssociation( object ): + def __init__( self, user, group ): + if isinstance( group, GroupDatasetAssociation ) or isinstance( group, DefaultUserGroupAssociation ) or isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.user = user + self.group = group + +class DefaultHistoryRoleAssociation( object ): + def __init__( self, history, role ): + if isinstance( role, RoleDatasetAssociation ) or isinstance( role, DefaultUserRoleAssociation ) or isinstance( role, DefaultHistoryRoleAssociation ): + role = role.role + self.history = history + self.role = role + +class DefaultHistoryGroupAssociation( object ): + def __init__( self, history, group ): + if isinstance( group, GroupDatasetAssociation ) or isinstance( group, DefaultUserGroupAssociation ) or isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.history = history + self.group = group + +class Dataset( object ): + states = Bunch( NEW = 'new', + QUEUED = 'queued', + RUNNING = 'running', + OK = 'ok', + EMPTY = 'empty', + ERROR = 'error', + DISCARDED = 'discarded' ) + access_actions = AccessRole.dataset_actions + file_path = "/tmp/" + engine = None + def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True, access_groups=[], access_roles=[] ): + self.id = id + self.state = state + self.deleted = False + self.purged = False + self.purgable = purgable + self.external_filename = external_filename + self._extra_files_path = extra_files_path + self.file_size = file_size + if access_groups or access_roles: + #self.flush() + for group in access_groups: + group.add_dataset( self ) + group.flush() + for role in access_roles: + role.add_dataset( self ) + role.flush() + def get_file_name( self ): + if not self.external_filename: + assert self.id is not None, "ID must be set before filename used (commit the object)" + # First try filename directly under file_path + filename = os.path.join( self.file_path, "dataset_%d.dat" % self.id ) + # Only use that filename if it already exists (backward compatibility), + # otherwise construct hashed path + if not os.path.exists( filename ): + dir = os.path.join( self.file_path, *directory_hash_id( self.id ) ) + # Create directory if it does not exist + try: + os.makedirs( dir ) + except OSError, e: + # File Exists is okay, otherwise reraise + if e.errno != errno.EEXIST: + raise + # Return filename inside hashed directory + return os.path.abspath( os.path.join( dir, "dataset_%d.dat" % self.id ) ) + else: + filename = self.external_filename + # Make filename absolute + return os.path.abspath( filename ) + + def set_file_name ( self, filename ): + if not filename: + self.external_filename = None + else: + self.external_filename = filename + + file_name = property( get_file_name, set_file_name ) + + @property + def extra_files_path( self ): + if self._extra_files_path: + path = self._extra_files_path + else: + path = os.path.join( self.file_path, "dataset_%d_files" % self.id ) + #only use path directly under self.file_path if it exists + if not os.path.exists( path ): + path = os.path.join( os.path.join( self.file_path, *directory_hash_id( self.id ) ), "dataset_%d_files" % self.id ) + # Make path absolute + return os.path.abspath( path ) + + def get_size( self ): + """Returns the size of the data on disk""" + if self.file_size: + return self.file_size + else: + try: + return os.path.getsize( self.file_name ) + except OSError: + return 0 + def set_size( self ): + """Returns the size of the data on disk""" + try: + self.file_size = os.path.getsize( self.file_name ) + except OSError: + self.file_size = 0 + def has_data( self ): + """Detects whether there is any data""" + return self.get_size() > 0 + def mark_deleted( self, include_children=True ): + self.deleted = True + def allow_action( self, user, action ): + """Returns true when user has permission to perform an action""" + + #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 roles is allowed + if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = GalaxyGroup.public_id, dataset_id = self.id ): + return True + elif user is not None: + #loop through permissions and if allowed return true: + #check roles associated directly with dataset first + for role_dataset_assoc in self.roles: + if action in role_dataset_assoc.role.actions and user.has_role( role_dataset_assoc.role ): + return True + #check roles associated with dataset through groups + for group_dataset_assoc in self.groups: + if user.has_group( group_dataset_assoc.group ): + for group_role_assoc in group_dataset_assoc.group.roles: + if action in group_role_assoc.role.actions: + return True + return False #no user and dataset not in public group, or user lacks permission + def guess_derived_groups_roles( self, other_datasets = [] ): + """Returns a list of output roles and groups based upon itself and provided datasets""" + if not other_datasets: + return [ data_group_assoc.group for data_group_assoc in self.groups ], [ data_role_assoc.role for data_role_assoc in self.roles ] + access_roles = None + priority_access_role = None + access_groups = None + priority_access_group = None + for dataset in [ self ] + other_datasets: + #determine access roles and groups for output datasets + #roles and groups for output dataset is the intersection across all inputs + #if we end up with no intersection between inputs, then we rely on priorities + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset + roles = [ data_role_assoc.role for data_role_assoc in dataset.roles ] + for role in roles: + if priority_access_role is None or priority_access_role.priority < role.priority: + priority_access_role = role + 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_roles is None: + access_roles = set( roles ) + access_groups = set( groups ) + else: + access_roles.intersection_update( set( roles ) ) + access_groups.intersection_update( set( groups ) ) + + #complete lists for output dataset access + if access_roles: + access_roles = list( access_roles ) + else: + access_roles = [] + if access_groups: + access_groups = list( access_groups) + else: + access_groups = [] + #if we have no roles or groups left after intersection, + #take the highest priority group or role + if not access_roles and not access_groups: + if priority_access_role and priority_access_group: + if priority_access_group.priority == priority_access_role.priority: + access_groups = [ priority_access_group ] + access_roles = [ priority_access_role ] + elif priority_access_group.priority > priority_access_role.priority: + access_groups = [ priority_access_group ] + else: + access_roles = [ priority_access_role ] + elif priority_access_role: + access_roles = [ priority_access_role ] + elif priority_access_group: + access_groups = [ priority_access_group ] + + return access_groups, access_roles + def add_group( self, group ): + return group.add_dataset( self ) + def add_role( self, role ): + return role.add_dataset( self ) + + def has_group( self, group ): + return bool( GroupDatasetAssociation.get_by( group_id = group.id, dataset_id = self.id ) ) + def has_role( self, role ): + return bool( RoleDatasetAssociation.get_by( role_id = role.id, dataset_id = self.id ) ) + + # FIXME: sqlalchemy will replace this + def _delete(self): + """Remove the file that corresponds to this data""" + try: + os.remove(self.data.file_name) + except OSError, e: + log.critical('%s delete error %s' % (self.__class__.__name__, e)) + + + class HistoryDatasetAssociation( object ): + states = Dataset.states + access_actions = Dataset.access_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, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): + parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, + visible=True, create_dataset = False, access_groups = [], access_roles = [] ): self.name = name or "Unnamed dataset" self.id = id self.hid = hid @@ -120,7 +528,7 @@ class HistoryDatasetAssociation( object ): # Relationships self.history = history if not dataset and create_dataset: - dataset = Dataset() + dataset = Dataset( access_groups = access_groups, access_roles = access_roles ) dataset.flush() self.dataset = dataset self.parent_id = parent_id @@ -131,10 +539,6 @@ class HistoryDatasetAssociation( object ): 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 ): @@ -252,7 +656,8 @@ class HistoryDatasetAssociation( object ): def get_converter_types(self): return self.datatype.get_converter_types( self, datatypes_registry) - def copy( self, copy_children = False, parent_id = None ): + def copy( self, copy_children = False, parent_id = None, target_user = None ): + if target_user is None: target_user = self.user 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: @@ -274,10 +679,12 @@ class HistoryDatasetAssociation( object ): for child in self.children: child.mark_deleted() + def allow_action( self, user, action ): + return self.dataset.allow_action( user, action ) class History( object ): - def __init__( self, id=None, name=None, user=None ): + def __init__( self, id=None, name=None, user=None, default_roles = [], default_groups = [] ): self.id = id self.name = name or "Unnamed history" self.deleted = False @@ -288,6 +695,18 @@ class History( object ): self.datasets = [] self.galaxy_sessions = [] + if not default_roles: + if user: + default_roles = user.default_roles + if not default_groups: + if user: + default_groups = user.default_groups + else: + default_groups = [ GalaxyGroup.get( GalaxyGroup.public_id ) ] + + + self.set_default_access( roles = default_roles, groups = default_groups ) + def _next_hid( self ): # TODO: override this with something in the database that ensures # better integrity @@ -326,18 +745,55 @@ class History( object ): self.genome_build = genome_build self.datasets.append( dataset ) - def copy(self): - des = History() + 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 - des.user_id = self.user_id for data in self.datasets: - new_data = data.copy( copy_children = True ) + new_data = data.copy( copy_children = True, target_user = target_user ) des.add_dataset( new_data ) new_data.flush() des.hid_counter = self.hid_counter des.flush() return des + + def set_default_access( self, groups = None, roles = None, dataset = False ): + if groups is not None: + for assoc in self.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + assoc = DefaultHistoryGroupAssociation( self, group ) + assoc.flush() + if roles is not None: + for assoc in self.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = DefaultHistoryRoleAssociation( self, role ) + assoc.flush() + if dataset: + for data in self.datasets: + for hda in data.dataset.history_associations: + if self.user and hda.history not in self.user.histories: + break + else: + if groups is not None: + for assoc in data.dataset.groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + group.add_dataset( data ) + if roles is not None: + for assoc in data.dataset.roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + role.add_dataset( data ) + + # class Query( object ): # def __init__( self, name=None, state=None, tool_parameters=None, history=None ): @@ -348,98 +804,6 @@ class History( object ): # self.history = history # self.datasets = [] -class Dataset( object ): - states = Bunch( NEW = 'new', - QUEUED = 'queued', - RUNNING = 'running', - OK = 'ok', - EMPTY = 'empty', - ERROR = 'error', - DISCARDED = 'discarded' ) - file_path = "/tmp/" - engine = None - def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): - self.id = id - self.state = state - self.deleted = False - self.purged = False - self.purgable = purgable - self.external_filename = external_filename - self._extra_files_path = extra_files_path - self.file_size = file_size - - def get_file_name( self ): - if not self.external_filename: - assert self.id is not None, "ID must be set before filename used (commit the object)" - # First try filename directly under file_path - filename = os.path.join( self.file_path, "dataset_%d.dat" % self.id ) - # Only use that filename if it already exists (backward compatibility), - # otherwise construct hashed path - if not os.path.exists( filename ): - dir = os.path.join( self.file_path, *directory_hash_id( self.id ) ) - # Create directory if it does not exist - try: - os.makedirs( dir ) - except OSError, e: - # File Exists is okay, otherwise reraise - if e.errno != errno.EEXIST: - raise - # Return filename inside hashed directory - return os.path.abspath( os.path.join( dir, "dataset_%d.dat" % self.id ) ) - else: - filename = self.external_filename - # Make filename absolute - return os.path.abspath( filename ) - - def set_file_name ( self, filename ): - if not filename: - self.external_filename = None - else: - self.external_filename = filename - - file_name = property( get_file_name, set_file_name ) - - @property - def extra_files_path( self ): - if self._extra_files_path: - path = self._extra_files_path - else: - path = os.path.join( self.file_path, "dataset_%d_files" % self.id ) - #only use path directly under self.file_path if it exists - if not os.path.exists( path ): - path = os.path.join( os.path.join( self.file_path, *directory_hash_id( self.id ) ), "dataset_%d_files" % self.id ) - # Make path absolute - return os.path.abspath( path ) - - def get_size( self ): - """Returns the size of the data on disk""" - if self.file_size: - return self.file_size - else: - try: - return os.path.getsize( self.file_name ) - except OSError: - return 0 - def set_size( self ): - """Returns the size of the data on disk""" - try: - self.file_size = os.path.getsize( self.file_name ) - except OSError: - self.file_size = 0 - def has_data( self ): - """Detects whether there is any data""" - return self.get_size() > 0 - def mark_deleted( self, include_children=True ): - self.deleted = True - - # FIXME: sqlalchemy will replace this - def _delete(self): - """Remove the file that corresponds to this data""" - try: - os.remove(self.data.file_name) - except OSError, e: - log.critical('%s delete error %s' % (self.__class__.__name__, e)) - class Old_Dataset( Dataset ): pass diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cdc8419c439..335ab183dd5 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -114,6 +114,99 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) +GalaxyGroup.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 ) ) + +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 ) ) + +AccessRole.table = Table( "access_role", 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( "actions", JSONType(), default=[] ), + Column( "priority", Integer ) ) + +UserRoleAssociation.table = Table( "user_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupRoleAssociation.table = Table( "group_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + +RoleDatasetAssociation.table = Table( "role_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + +RoleRoleAssociation.table = Table( "role_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "target_role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupRoleAccessAssociation.table = Table( "group_role_access_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + + +DefaultUserRoleAssociation.table = Table( "default_user_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + +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 ) ) + +DefaultHistoryRoleAssociation.table = Table( "default_history_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + +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 ) ) + Job.table = Table( "job", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -298,6 +391,56 @@ assign_mapper( context, User, User.table, collection_class=ordering_list( 'order_index' ) ) ) ) +assign_mapper( context, GalaxyGroup, GalaxyGroup.table, + properties=dict( users=relation( UserGroupAssociation ), + datasets=relation( GroupDatasetAssociation ) ) ) + +assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, + properties=dict( user=relation( User, backref = "groups" ), + group=relation( GalaxyGroup, backref = "users" ) ) ) + +assign_mapper( context, UserRoleAssociation, UserRoleAssociation.table, + properties=dict( role=relation( AccessRole, backref = "users" ), + user=relation( User, backref = "roles" ) ) ) + +assign_mapper( context, GroupRoleAssociation, GroupRoleAssociation.table, + properties=dict( role=relation( AccessRole, backref = "groups" ), + group=relation( GalaxyGroup, backref = "roles" ) ) ) + +assign_mapper( context, AccessRole, AccessRole.table ) + +assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "groups" ), + group=relation( GalaxyGroup, backref = "datasets" ) ) ) + +assign_mapper( context, RoleDatasetAssociation, RoleDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "roles" ), + role=relation( AccessRole ) ) ) + +assign_mapper( context, RoleRoleAssociation, RoleRoleAssociation.table, + properties=dict( role=relation( AccessRole, primaryjoin=( ( RoleRoleAssociation.table.c.role_id == AccessRole.table.c.id ) ) ), + target_role=relation( AccessRole, primaryjoin=( RoleRoleAssociation.table.c.target_role_id == AccessRole.table.c.id ), backref="roles" ) ) ) + +assign_mapper( context, GroupRoleAccessAssociation, GroupRoleAccessAssociation.table, + properties=dict( role=relation( AccessRole, backref="access_groups" ), + group=relation( GalaxyGroup, backref="access_roles" ) ) ) + +assign_mapper( context, DefaultUserRoleAssociation, DefaultUserRoleAssociation.table, + properties=dict( user=relation( User, backref = "default_roles" ), + role=relation( AccessRole ) ) ) + +assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, + properties=dict( user=relation( User, backref = "default_groups" ), + group=relation( GalaxyGroup ) ) ) + +assign_mapper( context, DefaultHistoryRoleAssociation, DefaultHistoryRoleAssociation.table, + properties=dict( history=relation( History, backref = "default_roles" ), + role=relation( AccessRole ) ) ) + +assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssociation.table, + properties=dict( history=relation( History, backref = "default_groups" ), + group=relation( GalaxyGroup ) ) ) + assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -411,6 +554,33 @@ 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 + #set up default table entries here, currently only exist for access controls + if result.AccessRole.count() == 0: + log.warning( "There were no access roles located, setting up default (public) access roles." ) + #create public group + public_group = result.GalaxyGroup( 'public' ) + public_group.flush() + #create public_all role + public_role = result.AccessRole( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.GalaxyGroup.access_actions.ADD_DATASET, result.GalaxyGroup.access_actions.REMOVE_DATASET ] ) + public_role.flush() + public_group.add_role( public_role ) + + #store public group id + GalaxyGroup.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions + #add all datasets to public group + for dataset in result.Dataset.select(): + public_group.add_dataset( dataset ) + + #loop through all current users and associate with the public group + #and create and associate with user's own group + for user in result.User.select(): + public_group.add_user( user ) + private_group = user.create_private_group() + user.set_default_access( groups = [ public_group, private_group ], roles = [], history = True, dataset = True ) + else: + #retrieve from database and store public group id, assume first created group is public + GalaxyGroup.public_id = result.GalaxyGroup.select( order_by = asc( result.GalaxyGroup.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions + log.debug( "Public Group identified as id = %s." % ( GalaxyGroup.public_id ) ) return result def get_suite(): diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index a56e6a829b4..9d21d33f803 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1084,7 +1084,7 @@ class Tool: if visible == "visible": visible = True 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 ) + child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1120,7 +1120,7 @@ class Tool: else: visible = False 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 ) + primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) 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..058caf07203 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -43,6 +43,8 @@ class DefaultToolAction( object ): assoc.flush() data = new_data break + if data and not data.allow_action( trans.user, data.access_actions.USE ): + 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 +81,16 @@ class DefaultToolAction( object ): data = NoneDataset( datatypes_registry = trans.app.datatypes_registry ) if data.dbkey not in [None, '?']: input_dbkey = data.dbkey + + #determine output dataset access list + existing_datasets = [ inp for inp in inp_data.values() if inp ] + if existing_datasets: + output_access_groups, output_access_roles = existing_datasets[0].dataset.guess_derived_groups_roles( existing_datasets[1:] ) + else: + #no valid inputs, we will use history defaults + output_access_roles = [ role.role for role in trans.history.default_roles ] + 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] @@ -117,7 +129,7 @@ class DefaultToolAction( object ): ext = output.format if ext == "input": ext = input_ext - data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) + data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True, access_groups=output_access_groups, access_roles=output_access_roles ) # Commit the dataset immediately so it gets database assigned unique id data.flush() # Create an empty file immediately @@ -183,6 +195,8 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: + if not dataset.allow_action( trans.user, dataset.access_actions.USE ): + 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..5c68786b6b1 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,7 +65,7 @@ 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 = trans.app.model.HistoryDatasetAssociation( create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) data.name = err_code data.extension = "txt" data.dbkey = "?" @@ -158,7 +158,7 @@ class UploadToolAction( object ): if info is None: info = 'uploaded %s file' %data_type - data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) 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..212a691d6aa 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -979,19 +979,25 @@ 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, AccessRole, GalaxyGroup, GroupRoleAssociation >>> from galaxy.util.bunch import Bunch >>> 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 ) ) + >>> role = AccessRole( 'test', list( AccessRole.dataset_actions.__dict__.values() ) ) + >>> role.flush() + >>> group = GalaxyGroup( 'test' ) + >>> group.flush() + >>> GalaxyGroup.public_id = group.id + >>> GroupRoleAssociation( group, role ).flush() + >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True, access_groups=[ group ] ) ) >>> 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 ) ) +
+ + <% checked = "" %> + %if not data.dataset.has_group( trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_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/root/history_common.mako b/templates/root/history_common.mako index 877705614ed..d7093121868 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 data.allow_action( trans.user, data.access_actions.VIEW ): +
You do not have permision to view this dataset.
+ %elif data_state == "queued":
Job is waiting to run
%elif data_state == "running":
Job is currently running
From 8fd301c2e66af7f5e95da7559c93b28961ded7a5 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:45:11 -0400 Subject: [PATCH 02/94] RECOMMIT: Allow users to change the default permissions assigned to new datasets (not relying on input) for their current history. --- lib/galaxy/web/controllers/history.py | 44 +++++++++++++++++++++++++++ templates/history/options.mako | 1 + templates/history/permissions.mako | 42 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 lib/galaxy/web/controllers/history.py create mode 100644 templates/history/permissions.mako diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py new file mode 100644 index 00000000000..6483b94850c --- /dev/null +++ b/lib/galaxy/web/controllers/history.py @@ -0,0 +1,44 @@ +from galaxy.web.base.controller import * +import logging + +log = logging.getLogger( __name__ ) + +class HistoryController( BaseController ): + @web.expose + def index( self, trans, **kwd ): + raise 'Unimplemented' + + @web.expose + def set_default_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the current history""" + #TODO: allow changing of default roles associated with history + if trans.user: + if 'set_permissions' in kwd: + """The user clicked the set_permissions button on the set_permissions 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.model.GalaxyGroup.get( 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: + history.set_default_access( groups = group_in ) + return trans.show_ok_message( 'Default history permissions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to this history's default permissions." ) + 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 permissions." ) diff --git a/templates/history/options.mako b/templates/history/options.mako index 4faa967742d..81cf68f30d6 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 permissions 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..315609189cd --- /dev/null +++ b/templates/history/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permissions + +%if trans.user: +
    +
    Change Default History Permissions
    +
    +
    +
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% cur_groups = [ assoc.group for assoc in trans.get_history().default_groups ] %> +
    + + + %for group in user_groups: + + %endfor +
    GroupInOut
    ${group.name}
    +
    + +
    + +
    + This will change the default permissions assigned to new datasets for your current history. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif \ No newline at end of file From 250c5ae7595566fad5c5ecab3bc01465292a5352 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:47:06 -0400 Subject: [PATCH 03/94] RECOMMIT: Allow users to specify the default permissions assigned to new histories. --- lib/galaxy/web/controllers/user.py | 34 ++++++++++++++++++++++++ templates/user/index.mako | 3 +++ templates/user/permissions.mako | 42 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 templates/user/permissions.mako diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 41890611abc..bc3801137d2 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -166,3 +166,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_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the new histories""" + #TODO: allow changing of default roles + if trans.user: + if 'set_permissions' in kwd: + """The user clicked the set_permissions button on the set_permissions form""" + group_in = [] + group_out = [] + #collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.model.GalaxyGroup.get( 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.user.set_default_access( groups = group_in ) + return trans.show_ok_message( 'Default new history permissions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to new history's default permissions." ) + 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 permissions." ) diff --git a/templates/user/index.mako b/templates/user/index.mako index 2b11262cece..64d23dd2410 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -8,6 +8,9 @@ %else: diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako new file mode 100644 index 00000000000..1b0803d44df --- /dev/null +++ b/templates/user/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permissions + +%if trans.user: +
    +
    Change Default Permissions for new Histories
    +
    +
    +
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% cur_groups = [ assoc.group for assoc in trans.user.default_groups ] %> +
    + + + %for group in user_groups: + + %endfor +
    GroupInOut
    ${group.name}
    +
    + +
    + +
    + This will change the default permissions assigned to new datasets for new histories. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif \ No newline at end of file From 80e56db29da96f851dcff768494987d4ac0b47ef Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:47:57 -0400 Subject: [PATCH 04/94] RECOMMIT: Removing newly created history controller, and moving history_set_permissions into the root controller with the rest of the history methods. --- lib/galaxy/web/controllers/history.py | 44 --------------------------- lib/galaxy/web/controllers/root.py | 36 ++++++++++++++++++++++ templates/history/options.mako | 2 +- 3 files changed, 37 insertions(+), 45 deletions(-) delete mode 100644 lib/galaxy/web/controllers/history.py diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py deleted file mode 100644 index 6483b94850c..00000000000 --- a/lib/galaxy/web/controllers/history.py +++ /dev/null @@ -1,44 +0,0 @@ -from galaxy.web.base.controller import * -import logging - -log = logging.getLogger( __name__ ) - -class HistoryController( BaseController ): - @web.expose - def index( self, trans, **kwd ): - raise 'Unimplemented' - - @web.expose - def set_default_permissions( self, trans, **kwd ): - """Sets the user's default permissions for the current history""" - #TODO: allow changing of default roles associated with history - if trans.user: - if 'set_permissions' in kwd: - """The user clicked the set_permissions button on the set_permissions 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.model.GalaxyGroup.get( 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: - history.set_default_access( groups = group_in ) - return trans.show_ok_message( 'Default history permissions have been changed.' ) - else: - return trans.show_error_message( "You did not specify any changes to this history's default permissions." ) - 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 permissions." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 0812ce714cf..bb6328b0a5d 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -620,6 +620,42 @@ class RootController( BaseController ): 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_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the current history""" + #TODO: allow changing of default roles associated with history + if trans.user: + if 'set_permissions' in kwd: + """The user clicked the set_permissions button on the set_permissions 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.model.GalaxyGroup.get( 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: + history.set_default_access( groups = group_in ) + return trans.show_ok_message( 'Default history permissions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to this history's default permissions." ) + 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 permissions." ) + + @web.expose def dataset_make_primary( self, trans, id=None): """Copies a dataset and makes primary""" diff --git a/templates/history/options.mako b/templates/history/options.mako index 81cf68f30d6..1bb2794a32b 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,7 +18,7 @@ %endif %if app.config.enable_beta_features:
  • Construct workflow from the current history
  • -
  • Change default permissions for the current history
  • +
  • Change default permissions for the current history
  • %endif
  • Share current history %endif From 4a2efcffd15c2a34d21b3cd952da8e838526f3e3 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Tue, 5 Aug 2008 16:35:14 -0400 Subject: [PATCH 05/94] Changes to access controls, mostly cosmetic. --- lib/galaxy/model/__init__.py | 212 +++++++++++++------------ lib/galaxy/model/mapping.py | 122 ++++++++------ lib/galaxy/tools/__init__.py | 8 +- lib/galaxy/tools/actions/__init__.py | 4 +- lib/galaxy/tools/actions/upload.py | 8 +- lib/galaxy/tools/parameters/basic.py | 31 ++-- lib/galaxy/web/controllers/async.py | 4 +- lib/galaxy/web/controllers/root.py | 8 +- lib/galaxy/web/controllers/user.py | 2 +- lib/galaxy/web/framework/__init__.py | 2 +- templates/dataset/edit_attributes.mako | 2 +- 11 files changed, 238 insertions(+), 165 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 0bed6bb7d19..d74ae8a0420 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -27,38 +27,16 @@ def set_datatypes_registry( d_registry ): datatypes_registry = d_registry class User( object ): - def __init__( self, email=None, password=None, groups = [], roles = [], default_groups = [], default_roles = [] ): + def __init__( self, email=None, password=None ): self.email = email self.password = password self.external = False # Relationships self.histories = [] - if not groups: - groups.append( GalaxyGroup.get( GalaxyGroup.public_id ) ) - default_groups.append( groups[-1] ) - default_groups.append( self.create_private_group() ) - group_id_added = [] - for group in groups: - if group.id not in group_id_added: - group.add_user( self ) - group_id_added.append( group.id ) - group_id_added = [] - for group in default_groups: - if group.id not in group_id_added: - user_group_assoc = DefaultUserGroupAssociation( self, group ) - user_group_assoc.flush() - group_id_added.append( group.id ) - role_id_added = [] - for role in roles: - if role.id not in role_id_added: - role.add_user( self ) - role_id_added.append( role.id ) - role_id_added = [] - for role in default_roles: - if role.id not in role_id_added: - role_group_assoc = DefaultUserRoleAssociation( self, role ) - role_group_assoc.flush() - role_id_added.append( role.id ) + + self.set_default_access() + self.add_group( Group.get_public_group() ) + def set_password_cleartext( self, cleartext ): """Set 'self.password' to the digest of 'cleartext'.""" self.password = sha.new( cleartext ).hexdigest() @@ -66,28 +44,42 @@ class User( object ): """Check if 'cleartext' matches 'self.password' when hashed.""" return self.password == sha.new( cleartext ).hexdigest() def create_private_group( self ): + #create roles for user modification of role + user_permission = Permission( "%s role modification" % self.email, list( Role.access_actions.__dict__.values() ) ) + user_permission.flush() + user_role = Role( "%s role modification" % self.email ) + user_role.flush() + user_role.add_permission( user_permission ) + #add role to user + user_role.add_user( self ) + user_role.add_control_role( user_role ) + + #create private group - group = GalaxyGroup( self.email, priority = 10 ) + group = Group( self.email, priority = 10 ) group.flush() + #create dataset permissions + dataset_permission = Permission( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ) ) + dataset_permission.flush() #create private dataset access role - role = AccessRole( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ), priority = 1 ) + role = Role( "%s dataset access" % self.email, priority = 10 ) + role.add_permission( dataset_permission ) role.flush() + #add control role to role + role.add_control_role( user_role ) #add role to group group.add_role( role ) - #create roles for user modification of role - user_role = AccessRole( "%s role modification" % self.email, list( AccessRole.access_actions.__dict__.values() ) ) - user_role.flush() - #add role to user - user_role.add_user( self ) - #add role to role - role.add_role( user_role ) - #create roles for user modification of group - group_role = AccessRole( "%s group modification" % self.email, list( GalaxyGroup.access_actions.__dict__.values() ) ) + group_permission = Permission( "%s group modification" % self.email, list( Group.access_actions.__dict__.values() ) ) + group_permission.flush() + group_role = Role( "%s group modification" % self.email ) group_role.flush() + #add control role to role + group_role.add_control_role( user_role ) + group_role.add_permission( group_permission ) #add role to group - group.add_access_role( group_role ) + group.add_control_role( group_role ) #associate role and user group_role.add_user( self ) @@ -102,6 +94,9 @@ class User( object ): def has_role( self, check_role ): return bool( UserRoleAssociation.get_by( role_id = check_role.id, user_id = self.id ) ) def set_default_access( self, groups = None, roles = None, history = False, dataset = False ): + if groups is None and roles is None: + groups = [ Group.get_public_group(), self.create_private_group() ] + roles = [] if groups is not None: for assoc in self.default_groups: #this is the association not the actual group assoc.delete() @@ -181,7 +176,7 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -class AccessRole( object ): +class Permission( object ): dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading USE = 'dataset_use', #use in jobs ADD_ROLE = 'dataset_add_role', #dataset can be added to roles @@ -200,14 +195,21 @@ class AccessRole( object ): ADD_ROLE = 'group_add_role', #add role to group REMOVE_ROLE = 'group_remove_role', #remove role from group ADD_USER = 'group_add_user' ) #add users to group - - access_actions = role_actions - - def __init__( self, name, actions, priority = 0 ): + def __init__( self, name = None, actions = [] ): self.name = name - if not isinstance( actions, list ): - actions = [ actions ] self.actions = actions + def add_action( self, action ): + if action not in self.actions: + return self.actions.append( action ) + raise 'action (%s) already exists in permissions list (%s: %s).' % ( action, self.id, self.actions ) + def remove_action( self, action ): + return self.actions.remove( action ) + +class Role( object ): + access_actions = Permission.role_actions + + def __init__( self, name, priority = 0 ): + self.name = name self.priority = priority def add_user( self, user ): assoc = UserRoleAssociation( user, self ) @@ -217,18 +219,27 @@ class AccessRole( object ): assoc = GroupRoleAssociation( group, self ) assoc.flush() return assoc - def add_role( self, role ): - assoc = RoleRoleAssociation( role, self ) + def add_permission( self, permission ): + assoc = RolePermissionAssociation( self, permission ) assoc.flush() return assoc def add_dataset( self, dataset ): assoc = RoleDatasetAssociation( self, dataset ) assoc.flush() return assoc + def add_control_role( self, role ): + assoc = RoleControlRoleAssociation( role, self ) + assoc.flush() + return assoc -class GalaxyGroup( object ): +class Group( object ): public_id = None - access_actions = AccessRole.group_actions + access_actions = Permission.group_actions + + @classmethod + def get_public_group( cls ): + return Group.get( cls.public_id ) + def __init__( self, name, priority = 0 ): self.name = name self.priority = priority @@ -238,26 +249,31 @@ class GalaxyGroup( object ): return assoc def add_role( self, role ): return role.add_group( self ) - def add_access_role( self, role ): - assoc = GroupRoleAccessAssociation( self, role ) - assoc.flush() - return assoc def add_dataset( self, dataset ): assoc = GroupDatasetAssociation( self, dataset ) assoc.flush() return assoc + def add_control_role( self, role ): + assoc = GroupControlRoleAssociation( self, role ) + assoc.flush() + return assoc + +class RolePermissionAssociation( object ): + def __init__( self, role, permission ): + self.role = role + self.permission = permission class UserGroupAssociation( object ): def __init__( self, user, group ): self.user = user self.group = group -class RoleRoleAssociation( object ): +class RoleControlRoleAssociation( object ): def __init__( self, role, target_role ): self.role = role self.target_role = target_role -class GroupRoleAccessAssociation( object ): +class GroupControlRoleAssociation( object ): def __init__( self, group, role ): self.group = group self.role = role @@ -328,10 +344,10 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) - access_actions = AccessRole.dataset_actions + access_actions = Permission.dataset_actions file_path = "/tmp/" engine = None - def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True, access_groups=[], access_roles=[] ): + def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): self.id = id self.state = state self.deleted = False @@ -340,14 +356,7 @@ class Dataset( object ): self.external_filename = external_filename self._extra_files_path = extra_files_path self.file_size = file_size - if access_groups or access_roles: - #self.flush() - for group in access_groups: - group.add_dataset( self ) - group.flush() - for role in access_roles: - role.add_dataset( self ) - role.flush() + def get_file_name( self ): if not self.external_filename: assert self.id is not None, "ID must be set before filename used (commit the object)" @@ -416,20 +425,23 @@ class Dataset( object ): #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 roles is allowed - if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = GalaxyGroup.public_id, dataset_id = self.id ): + if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = Group.public_id, dataset_id = self.id ): return True elif user is not None: #loop through permissions and if allowed return true: #check roles associated directly with dataset first for role_dataset_assoc in self.roles: - if action in role_dataset_assoc.role.actions and user.has_role( role_dataset_assoc.role ): - return True + if user.has_role( role_dataset_assoc.role ): + for permission in role_dataset_assoc.role.permissions: + if action in permission.permission.actions: + return True #check roles associated with dataset through groups for group_dataset_assoc in self.groups: if user.has_group( group_dataset_assoc.group ): for group_role_assoc in group_dataset_assoc.group.roles: - if action in group_role_assoc.role.actions: - return True + for permission in group_role_assoc.role.permissions: + if action in permission.permission.actions: + return True return False #no user and dataset not in public group, or user lacks permission def guess_derived_groups_roles( self, other_datasets = [] ): """Returns a list of output roles and groups based upon itself and provided datasets""" @@ -490,7 +502,22 @@ class Dataset( object ): return group.add_dataset( self ) def add_role( self, role ): return role.add_dataset( self ) - + def set_groups( self, groups ): + for assoc in self.groups: + assoc.delete() + assoc.flush() + for group in groups: + if not isinstance( group, Group ): + group = group.group + self.add_group( group ) + def set_roles( self, roles ): + for assoc in self.roles: + assoc.delete() + assoc.flush() + for role in roles: + if not isinstance( role, Role ): + role = role.role + self.add_role( role ) def has_group( self, group ): return bool( GroupDatasetAssociation.get_by( group_id = group.id, dataset_id = self.id ) ) def has_role( self, role ): @@ -512,7 +539,7 @@ 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, access_groups = [], access_roles = [] ): + visible=True, create_dataset = False ): self.name = name or "Unnamed dataset" self.id = id self.hid = hid @@ -528,7 +555,7 @@ class HistoryDatasetAssociation( object ): # Relationships self.history = history if not dataset and create_dataset: - dataset = Dataset( access_groups = access_groups, access_roles = access_roles ) + dataset = Dataset() dataset.flush() self.dataset = dataset self.parent_id = parent_id @@ -684,7 +711,7 @@ class HistoryDatasetAssociation( object ): class History( object ): - def __init__( self, id=None, name=None, user=None, default_roles = [], default_groups = [] ): + def __init__( self, id=None, name=None, user=None ): self.id = id self.name = name or "Unnamed history" self.deleted = False @@ -695,17 +722,7 @@ class History( object ): self.datasets = [] self.galaxy_sessions = [] - if not default_roles: - if user: - default_roles = user.default_roles - if not default_groups: - if user: - default_groups = user.default_groups - else: - default_groups = [ GalaxyGroup.get( GalaxyGroup.public_id ) ] - - - self.set_default_access( roles = default_roles, groups = default_groups ) + self.set_default_access() def _next_hid( self ): # TODO: override this with something in the database that ensures @@ -760,6 +777,13 @@ class History( object ): return des def set_default_access( self, groups = None, roles = None, dataset = False ): + if groups is None and roles is None: + if self.user: + groups = self.user.default_groups + roles = self.user.default_roles + else: + groups = [ Group.get_public_group() ] + roles = [] if groups is not None: for assoc in self.default_groups: #this is the association not the actual group assoc.delete() @@ -778,20 +802,12 @@ class History( object ): for data in self.datasets: for hda in data.dataset.history_associations: if self.user and hda.history not in self.user.histories: + data.dataset.set_groups( [ Group.get_public_group() ] ) + data.dataset.set_roles( [] ) break else: - if groups is not None: - for assoc in data.dataset.groups: #this is the association not the actual group - assoc.delete() - assoc.flush() - for group in groups: - group.add_dataset( data ) - if roles is not None: - for assoc in data.dataset.roles: #this is the association not the actual group - assoc.delete() - assoc.flush() - for role in roles: - role.add_dataset( data ) + data.dataset.set_groups( groups ) + data.dataset.set_roles( roles ) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 335ab183dd5..c3fe6561ffc 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -114,7 +114,7 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) -GalaxyGroup.table = Table( "galaxy_group", metadata, +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 ), @@ -128,25 +128,38 @@ UserGroupAssociation.table = Table( "user_group_association", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) -AccessRole.table = Table( "access_role", metadata, +Permission.table = Table( "permission", 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( "actions", JSONType(), default=[] ) ) + +Role.table = Table( "role", 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( "actions", JSONType(), default=[] ), Column( "priority", Integer ) ) +RolePermissionAssociation.table = Table( "role_permission_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), + Column( "permission_id", Integer, ForeignKey( "permission.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + UserRoleAssociation.table = Table( "user_role_association", metadata, Column( "id", Integer, primary_key=True ), Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) GroupRoleAssociation.table = Table( "group_role_association", metadata, Column( "id", Integer, primary_key=True ), Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) @@ -159,21 +172,21 @@ GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, RoleDatasetAssociation.table = Table( "role_dataset_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.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 ) ) -RoleRoleAssociation.table = Table( "role_role_association", metadata, +RoleControlRoleAssociation.table = Table( "role_control_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), - Column( "target_role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), + Column( "target_role_id", Integer, ForeignKey( "role.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) -GroupRoleAccessAssociation.table = Table( "group_role_access_association", metadata, +GroupControlRoleAssociation.table = Table( "group_control_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.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 ) ) @@ -181,7 +194,7 @@ GroupRoleAccessAssociation.table = Table( "group_role_access_association", metad DefaultUserRoleAssociation.table = Table( "default_user_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.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 ) ) @@ -195,7 +208,7 @@ DefaultUserGroupAssociation.table = Table( "default_user_group_association", met DefaultHistoryRoleAssociation.table = Table( "default_history_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.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 ) ) @@ -391,55 +404,62 @@ assign_mapper( context, User, User.table, collection_class=ordering_list( 'order_index' ) ) ) ) -assign_mapper( context, GalaxyGroup, GalaxyGroup.table, +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( GalaxyGroup, backref = "users" ) ) ) + group=relation( Group, backref = "users" ) ) ) assign_mapper( context, UserRoleAssociation, UserRoleAssociation.table, - properties=dict( role=relation( AccessRole, backref = "users" ), + properties=dict( role=relation( Role, backref = "users" ), user=relation( User, backref = "roles" ) ) ) assign_mapper( context, GroupRoleAssociation, GroupRoleAssociation.table, - properties=dict( role=relation( AccessRole, backref = "groups" ), - group=relation( GalaxyGroup, backref = "roles" ) ) ) + properties=dict( role=relation( Role, backref = "groups" ), + group=relation( Group, backref = "roles" ) ) ) + +assign_mapper( context, Permission, Permission.table ) + +assign_mapper( context, Role, Role.table ) + +assign_mapper( context, RolePermissionAssociation, RolePermissionAssociation.table, + properties=dict( role=relation( Role, backref = "permissions" ), + permission=relation( Permission, backref = "roles" ) ) ) -assign_mapper( context, AccessRole, AccessRole.table ) assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "groups" ), - group=relation( GalaxyGroup, backref = "datasets" ) ) ) + group=relation( Group, backref = "datasets" ) ) ) assign_mapper( context, RoleDatasetAssociation, RoleDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "roles" ), - role=relation( AccessRole ) ) ) + role=relation( Role ) ) ) -assign_mapper( context, RoleRoleAssociation, RoleRoleAssociation.table, - properties=dict( role=relation( AccessRole, primaryjoin=( ( RoleRoleAssociation.table.c.role_id == AccessRole.table.c.id ) ) ), - target_role=relation( AccessRole, primaryjoin=( RoleRoleAssociation.table.c.target_role_id == AccessRole.table.c.id ), backref="roles" ) ) ) +assign_mapper( context, RoleControlRoleAssociation, RoleControlRoleAssociation.table, + properties=dict( role=relation( Role, primaryjoin=( ( RoleControlRoleAssociation.table.c.role_id == Role.table.c.id ) ) ), + target_role=relation( Role, primaryjoin=( RoleControlRoleAssociation.table.c.target_role_id == Role.table.c.id ), backref="roles" ) ) ) -assign_mapper( context, GroupRoleAccessAssociation, GroupRoleAccessAssociation.table, - properties=dict( role=relation( AccessRole, backref="access_groups" ), - group=relation( GalaxyGroup, backref="access_roles" ) ) ) +assign_mapper( context, GroupControlRoleAssociation, GroupControlRoleAssociation.table, + properties=dict( role=relation( Role, backref="access_groups" ), + group=relation( Group, backref="access_roles" ) ) ) assign_mapper( context, DefaultUserRoleAssociation, DefaultUserRoleAssociation.table, properties=dict( user=relation( User, backref = "default_roles" ), - role=relation( AccessRole ) ) ) + role=relation( Role ) ) ) assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, properties=dict( user=relation( User, backref = "default_groups" ), - group=relation( GalaxyGroup ) ) ) + group=relation( Group ) ) ) assign_mapper( context, DefaultHistoryRoleAssociation, DefaultHistoryRoleAssociation.table, properties=dict( history=relation( History, backref = "default_roles" ), - role=relation( AccessRole ) ) ) + role=relation( Role ) ) ) assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssociation.table, properties=dict( history=relation( History, backref = "default_groups" ), - group=relation( GalaxyGroup ) ) ) + group=relation( Group ) ) ) assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -555,32 +575,42 @@ def init( file_path, url, engine_options={}, create_tables=False ): result.context = context result.create_tables = create_tables #set up default table entries here, currently only exist for access controls - if result.AccessRole.count() == 0: + if result.Role.count() == 0: log.warning( "There were no access roles located, setting up default (public) access roles." ) #create public group - public_group = result.GalaxyGroup( 'public' ) + public_group = result.Group( 'public' ) public_group.flush() #create public_all role - public_role = result.AccessRole( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.GalaxyGroup.access_actions.ADD_DATASET, result.GalaxyGroup.access_actions.REMOVE_DATASET ] ) + public_role = result.Role( 'public' ) public_role.flush() public_group.add_role( public_role ) + permission = result.Permission( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.Group.access_actions.ADD_DATASET, result.Group.access_actions.REMOVE_DATASET ] ) + permission.flush() + public_role.add_permission( permission ) #store public group id - GalaxyGroup.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions - #add all datasets to public group - for dataset in result.Dataset.select(): - public_group.add_dataset( dataset ) + Group.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions - #loop through all current users and associate with the public group - #and create and associate with user's own group - for user in result.User.select(): - public_group.add_user( user ) - private_group = user.create_private_group() - user.set_default_access( groups = [ public_group, private_group ], roles = [], history = True, dataset = True ) + #loop through all histories and set up rbac on users, histories and datasets + for history in result.History.select(): + if history.user: + if not history.user.default_groups: + history.user.set_default_access( history = True, dataset = True ) + history.user.add_group( public_group ) + history.user.flush() + else: + history.set_default_access( 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: + dataset.set_groups( [ public_group ] ) + dataset.set_roles( [] ) else: #retrieve from database and store public group id, assume first created group is public - GalaxyGroup.public_id = result.GalaxyGroup.select( order_by = asc( result.GalaxyGroup.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions - log.debug( "Public Group identified as id = %s." % ( GalaxyGroup.public_id ) ) + Group.public_id = result.Group.select( order_by = asc( result.Group.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions + log.debug( "Public Group identified as id = %s." % ( Group.public_id ) ) return result def get_suite(): diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 9d21d33f803..af94a3a24ce 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1084,7 +1084,9 @@ class Tool: if visible == "visible": visible = True 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, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) + child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + child_dataset.dataset.set_groups( outdata.dataset.groups ) + child_dataset.dataset.set_roles( outdata.dataset.roles ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1120,7 +1122,9 @@ class Tool: else: visible = False 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, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) + primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + primary_data.dataset.set_groups( outdata.dataset.groups ) + primary_data.dataset.set_roles( outdata.dataset.roles ) 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 058caf07203..25393b29b7f 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -129,7 +129,9 @@ class DefaultToolAction( object ): ext = output.format if ext == "input": ext = input_ext - data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True, access_groups=output_access_groups, access_roles=output_access_roles ) + data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) + data.dataset.set_groups( output_access_groups ) + data.dataset.set_roles( output_access_roles ) # Commit the dataset immediately so it gets database assigned unique id data.flush() # Create an empty file immediately diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 5c68786b6b1..6d7675a9456 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,7 +65,9 @@ class UploadToolAction( object ): return dict( output=data_list[0] ) def upload_empty(self, trans, err_code, err_msg): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) + data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) + data.dataset.set_groups( trans.history.default_groups ) + data.dataset.set_roles( trans.history.default_roles ) data.name = err_code data.extension = "txt" data.dbkey = "?" @@ -158,7 +160,9 @@ class UploadToolAction( object ): if info is None: info = 'uploaded %s file' %data_type - data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) + data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + data.dataset.set_groups( trans.history.default_groups ) + data.dataset.set_roles( trans.history.default_roles ) 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 212a691d6aa..379529b8cab 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -979,21 +979,34 @@ 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, User, AccessRole, GalaxyGroup, GroupRoleAssociation + >>> from galaxy.model import History, HistoryDatasetAssociation, User, Role, Permission, Group, GroupRoleAssociation >>> from galaxy.util.bunch import Bunch >>> hist = History() >>> hist.flush() - >>> role = AccessRole( 'test', list( AccessRole.dataset_actions.__dict__.values() ) ) + >>> permission = Permission( 'test', list( Permission.dataset_actions.__dict__.values() ) ) + >>> permission.flush() + >>> role = Role( 'test' ) >>> role.flush() - >>> group = GalaxyGroup( 'test' ) + >>> assoc = role.add_permission( permission ) + >>> group = Group( 'test' ) >>> group.flush() - >>> GalaxyGroup.public_id = group.id + >>> Group.public_id = group.id >>> GroupRoleAssociation( group, role ).flush() - >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True, access_groups=[ group ] ) ) + >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) + >>> dataset1.dataset.set_groups( [ group ] ) + >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) + >>> dataset2.dataset.set_groups( [ group ] ) + >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) + >>> dataset3.dataset.set_groups( [ group ] ) + >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) + >>> dataset4.dataset.set_groups( [ group ] ) + >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) + >>> dataset5.dataset.set_groups( [ 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 diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index 4d68fb8eb0c..9278e5f3cf6 100644 --- a/lib/galaxy/web/controllers/async.py +++ b/lib/galaxy/web/controllers/async.py @@ -103,7 +103,9 @@ class ASync( BaseController ): #data.state = jobs.JOB_OK #history.datasets.add_dataset( data ) - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) + data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE ) + data.dataset.set_groups( trans.history.default_groups ) + data.dataset.set_roles( trans.history.default_roles ) data.name = GALAXY_NAME data.dbkey = GALAXY_BUILD data.info = GALAXY_INFO diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index bb6328b0a5d..3a9f9471ef5 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -265,7 +265,7 @@ class RootController( BaseController ): if not trans.user: return trans.show_error_message( "You must be logged in if you want to change dataset permissions." ) private_dataset = 'private_dataset' - public_group = trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_id ) + public_group = trans.app.model.Group.get_public_group() if private_dataset in kwd and data.dataset.has_group( public_group ): #check user has permision and then remove public group if data.dataset.allow_action( trans.user, data.dataset.access_actions.REMOVE_GROUP ): @@ -599,7 +599,9 @@ class RootController( BaseController ): copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) roles = copy_access_from.dataset.roles groups = copy_access_from.dataset.groups - data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True, access_groups = groups, access_roles = roles ) + data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True ) + data.dataset.set_groups( groups ) + data.dataset.set_roles( roles ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -633,7 +635,7 @@ class RootController( BaseController ): #collect groups as entered by user for name, value in kwd.items(): if name.startswith( "group_" ): - group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + group = trans.app.model.Group.get( name.replace( "group_", "", 1 ) ) if not group: return trans.show_error_message( 'You have specified an invalid group.' ) if value == 'in': diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index bc3801137d2..8de8eff2da0 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -179,7 +179,7 @@ class User( BaseController ): #collect groups as entered by user for name, value in kwd.items(): if name.startswith( "group_" ): - group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + group = trans.app.model.Group.get( name.replace( "group_", "", 1 ) ) if not group: return trans.show_error_message( 'You have specified an invalid group.' ) if value == 'in': diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index e3874e63248..111ac1a4f43 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -433,7 +433,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if history is not None and user is not None: if not history.user: #This user will now aquire previously unowned history, let set permissions to user's default - history.set_default_access( roles = [ role.role for role in user.default_roles ], groups = [ group.group for group in user.default_groups ], dataset = True ) + history.set_default_access( roles = user.default_roles, groups = user.default_groups, dataset = True ) history.user_id = user.id history.flush() self.__history = history diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index e159c89b3a6..aa5e0d23370 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -144,7 +144,7 @@ Private Dataset: <% checked = "" %> - %if not data.dataset.has_group( trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_id ) ): + %if not data.dataset.has_group( trans.app.model.Group.get_public_group() ): <% checked = " checked" %> %endif
    From 32cb3c53414eda4cf69f7c0d520fa29427d0e447 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Wed, 6 Aug 2008 15:22:02 -0400 Subject: [PATCH 06/94] Moved security code out of model and into its own directory. --- lib/galaxy/app.py | 3 + lib/galaxy/model/__init__.py | 271 +----------------- lib/galaxy/model/mapping.py | 16 +- lib/galaxy/security/__init__.py | 362 +++++++++++++++++++++++++ lib/galaxy/tools/__init__.py | 8 +- lib/galaxy/tools/actions/__init__.py | 10 +- lib/galaxy/tools/actions/upload.py | 10 +- lib/galaxy/tools/parameters/basic.py | 23 +- lib/galaxy/web/controllers/async.py | 4 +- lib/galaxy/web/controllers/dataset.py | 2 +- lib/galaxy/web/controllers/root.py | 34 +-- lib/galaxy/web/controllers/user.py | 3 +- lib/galaxy/web/framework/__init__.py | 3 +- templates/dataset/edit_attributes.mako | 2 +- templates/root/history_common.mako | 2 +- 15 files changed, 433 insertions(+), 320 deletions(-) create mode 100644 lib/galaxy/security/__init__.py 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/model/__init__.py b/lib/galaxy/model/__init__.py index d74ae8a0420..6e0d0922ea6 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__ ) @@ -34,86 +35,12 @@ class User( object ): # Relationships self.histories = [] - self.set_default_access() - self.add_group( Group.get_public_group() ) - 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() - def create_private_group( self ): - #create roles for user modification of role - user_permission = Permission( "%s role modification" % self.email, list( Role.access_actions.__dict__.values() ) ) - user_permission.flush() - user_role = Role( "%s role modification" % self.email ) - user_role.flush() - user_role.add_permission( user_permission ) - #add role to user - user_role.add_user( self ) - user_role.add_control_role( user_role ) - - - #create private group - group = Group( self.email, priority = 10 ) - group.flush() - #create dataset permissions - dataset_permission = Permission( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ) ) - dataset_permission.flush() - #create private dataset access role - role = Role( "%s dataset access" % self.email, priority = 10 ) - role.add_permission( dataset_permission ) - role.flush() - #add control role to role - role.add_control_role( user_role ) - #add role to group - group.add_role( role ) - - #create roles for user modification of group - group_permission = Permission( "%s group modification" % self.email, list( Group.access_actions.__dict__.values() ) ) - group_permission.flush() - group_role = Role( "%s group modification" % self.email ) - group_role.flush() - #add control role to role - group_role.add_control_role( user_role ) - group_role.add_permission( group_permission ) - #add role to group - group.add_control_role( group_role ) - #associate role and user - group_role.add_user( self ) - - #add user to group - group.add_user( self ) - group.flush() - return group - def add_group( self, group ): - return group.add_user( self ) - def has_group( self, check_group ): - return bool( UserGroupAssociation.get_by( group_id = check_group.id, user_id = self.id ) ) - def has_role( self, check_role ): - return bool( UserRoleAssociation.get_by( role_id = check_role.id, user_id = self.id ) ) - def set_default_access( self, groups = None, roles = None, history = False, dataset = False ): - if groups is None and roles is None: - groups = [ Group.get_public_group(), self.create_private_group() ] - roles = [] - if groups is not None: - for assoc in self.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() - for group in groups: - assoc = DefaultUserGroupAssociation( self, group ) - assoc.flush() - if roles is not None: - for assoc in self.default_roles: #this is the association not the actual group - assoc.delete() - assoc.flush() - for role in roles: - assoc = DefaultUserRoleAssociation( self, role ) - assoc.flush() - if history: - for history in self.histories: - history.set_default_access( groups = groups, roles = roles, dataset = dataset ) class Job( object ): """ @@ -177,24 +104,10 @@ class JobToOutputDatasetAssociation( object ): self.dataset = dataset class Permission( object ): - dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading - USE = 'dataset_use', #use in jobs - ADD_ROLE = 'dataset_add_role', #dataset can be added to roles - REMOVE_ROLE = 'dataset_remove_role', #dataset can be removed from roles - ADD_GROUP = 'dataset_add_group', #dataset can be added to groups - REMOVE_GROUP = 'dataset_remove_group' ) #dataset can be removed from groups - role_actions = Bunch( ADD_DATASET = 'role_add_dataset', #add role to dataset - REMOVE_DATASET = 'role_remove_dataset', #remove role from dataset - DELETE = 'role_delete', #delete a role - MODIFY = 'role_modify', #change a role's actions, - ADD_GROUP = 'role_add_group', #add role to a group - REMOVE_GROUP = 'role_remove_group' ) #remove role from a group - group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add group to dataset - REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group - DELETE = 'group_delete', #delete a group - ADD_ROLE = 'group_add_role', #add role to group - REMOVE_ROLE = 'group_remove_role', #remove role from group - ADD_USER = 'group_add_user' ) #add users to group + dataset_actions = RBACAgent.actions.dataset_actions + role_actions = RBACAgent.actions.role_actions + group_actions = RBACAgent.actions.group_actions + def __init__( self, name = None, actions = [] ): self.name = name self.actions = actions @@ -211,26 +124,6 @@ class Role( object ): def __init__( self, name, priority = 0 ): self.name = name self.priority = priority - def add_user( self, user ): - assoc = UserRoleAssociation( user, self ) - assoc.flush() - return assoc - def add_group( self, group ): - assoc = GroupRoleAssociation( group, self ) - assoc.flush() - return assoc - def add_permission( self, permission ): - assoc = RolePermissionAssociation( self, permission ) - assoc.flush() - return assoc - def add_dataset( self, dataset ): - assoc = RoleDatasetAssociation( self, dataset ) - assoc.flush() - return assoc - def add_control_role( self, role ): - assoc = RoleControlRoleAssociation( role, self ) - assoc.flush() - return assoc class Group( object ): public_id = None @@ -243,20 +136,6 @@ class Group( object ): def __init__( self, name, priority = 0 ): self.name = name self.priority = priority - def add_user( self, user ): - assoc = UserGroupAssociation( user, self ) - assoc.flush() - return assoc - def add_role( self, role ): - return role.add_group( self ) - def add_dataset( self, dataset ): - assoc = GroupDatasetAssociation( self, dataset ) - assoc.flush() - return assoc - def add_control_role( self, role ): - assoc = GroupControlRoleAssociation( self, role ) - assoc.flush() - return assoc class RolePermissionAssociation( object ): def __init__( self, role, permission ): @@ -420,108 +299,6 @@ class Dataset( object ): return self.get_size() > 0 def mark_deleted( self, include_children=True ): self.deleted = True - def allow_action( self, user, action ): - """Returns true when user has permission to perform an action""" - - #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 roles is allowed - if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = Group.public_id, dataset_id = self.id ): - return True - elif user is not None: - #loop through permissions and if allowed return true: - #check roles associated directly with dataset first - for role_dataset_assoc in self.roles: - if user.has_role( role_dataset_assoc.role ): - for permission in role_dataset_assoc.role.permissions: - if action in permission.permission.actions: - return True - #check roles associated with dataset through groups - for group_dataset_assoc in self.groups: - if user.has_group( group_dataset_assoc.group ): - for group_role_assoc in group_dataset_assoc.group.roles: - for permission in group_role_assoc.role.permissions: - if action in permission.permission.actions: - return True - return False #no user and dataset not in public group, or user lacks permission - def guess_derived_groups_roles( self, other_datasets = [] ): - """Returns a list of output roles and groups based upon itself and provided datasets""" - if not other_datasets: - return [ data_group_assoc.group for data_group_assoc in self.groups ], [ data_role_assoc.role for data_role_assoc in self.roles ] - access_roles = None - priority_access_role = None - access_groups = None - priority_access_group = None - for dataset in [ self ] + other_datasets: - #determine access roles and groups for output datasets - #roles and groups for output dataset is the intersection across all inputs - #if we end up with no intersection between inputs, then we rely on priorities - if isinstance( dataset, HistoryDatasetAssociation ): - dataset = dataset.dataset - roles = [ data_role_assoc.role for data_role_assoc in dataset.roles ] - for role in roles: - if priority_access_role is None or priority_access_role.priority < role.priority: - priority_access_role = role - 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_roles is None: - access_roles = set( roles ) - access_groups = set( groups ) - else: - access_roles.intersection_update( set( roles ) ) - access_groups.intersection_update( set( groups ) ) - - #complete lists for output dataset access - if access_roles: - access_roles = list( access_roles ) - else: - access_roles = [] - if access_groups: - access_groups = list( access_groups) - else: - access_groups = [] - #if we have no roles or groups left after intersection, - #take the highest priority group or role - if not access_roles and not access_groups: - if priority_access_role and priority_access_group: - if priority_access_group.priority == priority_access_role.priority: - access_groups = [ priority_access_group ] - access_roles = [ priority_access_role ] - elif priority_access_group.priority > priority_access_role.priority: - access_groups = [ priority_access_group ] - else: - access_roles = [ priority_access_role ] - elif priority_access_role: - access_roles = [ priority_access_role ] - elif priority_access_group: - access_groups = [ priority_access_group ] - - return access_groups, access_roles - def add_group( self, group ): - return group.add_dataset( self ) - def add_role( self, role ): - return role.add_dataset( self ) - def set_groups( self, groups ): - for assoc in self.groups: - assoc.delete() - assoc.flush() - for group in groups: - if not isinstance( group, Group ): - group = group.group - self.add_group( group ) - def set_roles( self, roles ): - for assoc in self.roles: - assoc.delete() - assoc.flush() - for role in roles: - if not isinstance( role, Role ): - role = role.role - self.add_role( role ) - def has_group( self, group ): - return bool( GroupDatasetAssociation.get_by( group_id = group.id, dataset_id = self.id ) ) - def has_role( self, role ): - return bool( RoleDatasetAssociation.get_by( role_id = role.id, dataset_id = self.id ) ) # FIXME: sqlalchemy will replace this def _delete(self): @@ -706,9 +483,6 @@ class HistoryDatasetAssociation( object ): for child in self.children: child.mark_deleted() - def allow_action( self, user, action ): - return self.dataset.allow_action( user, action ) - class History( object ): def __init__( self, id=None, name=None, user=None ): @@ -722,8 +496,6 @@ class History( object ): self.datasets = [] self.galaxy_sessions = [] - self.set_default_access() - def _next_hid( self ): # TODO: override this with something in the database that ensures # better integrity @@ -776,39 +548,6 @@ class History( object ): des.flush() return des - def set_default_access( self, groups = None, roles = None, dataset = False ): - if groups is None and roles is None: - if self.user: - groups = self.user.default_groups - roles = self.user.default_roles - else: - groups = [ Group.get_public_group() ] - roles = [] - if groups is not None: - for assoc in self.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() - for group in groups: - assoc = DefaultHistoryGroupAssociation( self, group ) - assoc.flush() - if roles is not None: - for assoc in self.default_roles: #this is the association not the actual group - assoc.delete() - assoc.flush() - for role in roles: - assoc = DefaultHistoryRoleAssociation( self, role ) - assoc.flush() - if dataset: - for data in self.datasets: - for hda in data.dataset.history_associations: - if self.user and hda.history not in self.user.histories: - data.dataset.set_groups( [ Group.get_public_group() ] ) - data.dataset.set_roles( [] ) - break - else: - data.dataset.set_groups( groups ) - data.dataset.set_roles( roles ) - # class Query( object ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index c3fe6561ffc..746ec2f622d 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 ) @@ -574,6 +575,8 @@ 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 ) #set up default table entries here, currently only exist for access controls if result.Role.count() == 0: log.warning( "There were no access roles located, setting up default (public) access roles." ) @@ -583,10 +586,10 @@ def init( file_path, url, engine_options={}, create_tables=False ): #create public_all role public_role = result.Role( 'public' ) public_role.flush() - public_group.add_role( public_role ) + result.security_agent.associate_components( group = public_group, role = public_role ) permission = result.Permission( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.Group.access_actions.ADD_DATASET, result.Group.access_actions.REMOVE_DATASET ] ) permission.flush() - public_role.add_permission( permission ) + result.security_agent.associate_components( permission = permission, role = public_role ) #store public group id Group.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions @@ -595,18 +598,17 @@ def init( file_path, url, engine_options={}, create_tables=False ): for history in result.History.select(): if history.user: if not history.user.default_groups: - history.user.set_default_access( history = True, dataset = True ) - history.user.add_group( public_group ) + results.security_agent.setup_new_user( history.user ) history.user.flush() else: - history.set_default_access( dataset = True ) + 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: - dataset.set_groups( [ public_group ] ) - dataset.set_roles( [] ) + result.security_agent.set_dataset_groups( dataset, [ public_group ] ) + result.security_agent.set_dataset_roles( dataset, [] ) else: #retrieve from database and store public group id, assume first created group is public Group.public_id = result.Group.select( order_by = asc( result.Group.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..fa56d3a4310 --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,362 @@ +""" +Utility functions used systemwide. + +""" +import logging +from galaxy.util.bunch import Bunch + +log = logging.getLogger(__name__) + +class RBACAgent: + """Class that handles galaxy security""" + + actions = Bunch( + dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading + USE = 'dataset_use', #use in jobs + ADD_ROLE = 'dataset_add_role', #dataset can be added to roles + REMOVE_ROLE = 'dataset_remove_role', #dataset can be removed from roles + ADD_GROUP = 'dataset_add_group', #dataset can be added to groups + REMOVE_GROUP = 'dataset_remove_group' ), #dataset can be removed from groups + role_actions = Bunch( ADD_DATASET = 'role_add_dataset', #add role to dataset + REMOVE_DATASET = 'role_remove_dataset', #remove role from dataset + DELETE = 'role_delete', #delete a role + MODIFY = 'role_modify', #change a role's actions, + ADD_GROUP = 'role_add_group', #add role to a group + REMOVE_GROUP = 'role_remove_group' ), #remove role from a group + group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add group to dataset + REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group + DELETE = 'group_delete', #delete a group + ADD_ROLE = 'group_add_role', #add role to group + REMOVE_ROLE = 'group_remove_role', #remove role from group + ADD_USER = 'group_add_user' ) #add users to group + ) + + 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_roles_for_datasets( self, datasets = [] ): + raise "Unimplemented Method" + def associate_components( self, **kwd ): + raise 'No valid method of associating provided components: %s' % kwd + def create_private_user_group( self, user ): + raise "Unimplemented Method" + def user_set_default_access( self, user, groups = None, roles = 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, roles = None, dataset = False ): + raise "Unimplemented Method" + def get_public_group( self ): + raise "Unimplemented Method" + def set_dataset_groups( self, dataset, groups ): + raise "Unimplemented Method" + def set_dataset_roles( self, dataset, roles ): + 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, actions = None ): + self.model = model + if actions: + actions = 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""" + + while 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 roles is allowed + if action in [ self.actions.dataset_actions.USE, self.actions.dataset_actions.VIEW ] and self.components_are_associated( group = self.get_public_group(), dataset = dataset ): + return True + elif user is not None: + #loop through permissions and if allowed return true: + #check roles associated directly with dataset first + for role_dataset_assoc in dataset.roles: + if self.components_are_associated( user = user, role = role_dataset_assoc.role ): + for permission in role_dataset_assoc.role.permissions: + if action in permission.permission.actions: + return True + #check roles associated with dataset through groups + for group_dataset_assoc in dataset.groups: + if self.components_are_associated( user = user, group = group_dataset_assoc.group ): + for group_role_assoc in group_dataset_assoc.group.roles: + for permission in group_role_assoc.role.permissions: + if action in permission.permission.actions: + return True + return False #no user and dataset not in public group, or user lacks permission + def guess_derived_groups_roles_for_datasets( self, datasets = [] ): + """Returns a list of output roles and groups based upon itself and provided datasets""" + access_roles = None + priority_access_role = None + access_groups = None + priority_access_group = None + for dataset in datasets: + #determine access roles and groups for output datasets + #roles and groups for output dataset is 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 + roles = [ data_role_assoc.role for data_role_assoc in dataset.roles ] + for role in roles: + if priority_access_role is None or priority_access_role.priority < role.priority: + priority_access_role = role + 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_roles is None: + access_roles = set( roles ) + access_groups = set( groups ) + else: + access_roles.intersection_update( set( roles ) ) + access_groups.intersection_update( set( groups ) ) + + #complete lists for output dataset access + if access_roles: + access_roles = list( access_roles ) + else: + access_roles = [] + if access_groups: + access_groups = list( access_groups) + else: + access_groups = [] + #if we have no roles or groups left after intersection, + #take the highest priority group or role + if not access_roles and not access_groups: + if priority_access_role and priority_access_group: + if priority_access_group.priority == priority_access_role.priority: + access_groups = [ priority_access_group ] + access_roles = [ priority_access_role ] + elif priority_access_group.priority > priority_access_role.priority: + access_groups = [ priority_access_group ] + else: + access_roles = [ priority_access_role ] + elif priority_access_role: + access_roles = [ priority_access_role ] + elif priority_access_group: + access_groups = [ priority_access_group ] + + return access_groups, access_roles + + 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 'role' in kwd: + return self.associate_role_dataset( kwd['role'], kwd['dataset'] ) + elif 'user' in kwd: + if 'group' in kwd: + return self.associate_user_group( kwd['user'], kwd['group'] ) + elif 'role' in kwd: + return self.associate_user_role( kwd['user'], kwd['role'] ) + elif 'role' in kwd: + if 'group' in kwd: + return self.associate_group_role( kwd['group'], kwd['role'] ) + elif 'control_role' in kwd: + return self.associate_role_control_role( kwd['control_role'], kwd['role'] ) + elif 'target_role' in kwd: + return self.associate_role_control_role( kwd['role'], kwd['target_role'] ) + elif 'permission' in kwd: + return self.associate_role_permission( kwd['role'], kwd['permission'] ) + elif 'group' in kwd: + if 'control_role' in kwd: + return self.associate_group_control_role( kwd['group'], kwd['control_role'] ) + raise 'No valid method of associating provided components: %s' % kwd + def associate_group_dataset( self, group, dataset ): + assoc = self.model.GroupDatasetAssociation( group, dataset ) + assoc.flush() + return assoc + def associate_role_dataset( self, role, dataset ): + assoc = self.model.RoleDatasetAssociation( role, dataset ) + assoc.flush() + return assoc + def associate_user_group( self, user, group ): + assoc = self.model.UserGroupAssociation( user, group ) + assoc.flush() + return assoc + def associate_user_role( self, user, role ): + assoc = self.model.UserRoleAssociation( user, role ) + assoc.flush() + return assoc + def associate_group_role( self, group, role ): + assoc = self.model.GroupRoleAssociation( group, role ) + assoc.flush() + return assoc + def associate_role_control_role( self, control_role, role ): + assoc = self.model.RoleControlRoleAssociation( control_role, role ) + assoc.flush() + return assoc + def associate_group_control_role( self, group, role ): + assoc = self.model.GroupControlRoleAssociation( group, role ) + assoc.flush() + return assoc + def associate_role_permission( self, role, permission ): + assoc = self.model.RolePermissionAssociation( role, permission ) + assoc.flush() + return assoc + + def create_private_user_group( self, user ): + #create roles for user modification of role + user_permission = self.model.Permission( "%s role modification" % user.email, list( self.model.Role.access_actions.__dict__.values() ) ) + user_permission.flush() + user_role = self.model.Role( "%s role modification" % user.email ) + user_role.flush() + self.associate_components( role = user_role, permission = user_permission ) + self.associate_components( user = user, role = user_role ) + self.associate_components( control_role = user_role, role = user_role ) + + + #create private group + group = self.model.Group( user.email, priority = 10 ) + group.flush() + #create dataset permissions + dataset_permission = self.model.Permission( "%s dataset access" % user.email, list( self.model.Dataset.access_actions.__dict__.values() ) ) + dataset_permission.flush() + #create private dataset access role + role = self.model.Role( "%s dataset access" % user.email, priority = 10 ) + self.associate_components( role = role, permission = dataset_permission ) + role.flush() + #add control role to role + self.associate_components( control_role = user_role, role = role ) + #add role to group + self.associate_components( group = group, role = user_role ) + + #create roles for user modification of group + group_permission = self.model.Permission( "%s group modification" % user.email, list( self.model.Group.access_actions.__dict__.values() ) ) + group_permission.flush() + group_role = self.model.Role( "%s group modification" % user.email ) + group_role.flush() + #add control role to role + self.associate_components( control_role = user_role, role = group_role ) + self.associate_components( permission = group_permission, role = group_role ) + #add role to group + self.associate_components( control_role = group_role, group = group ) + #associate role and user + self.associate_components( role = group_role, user = user ) + + #add user to group + self.associate_components( group = group, user = user ) + group.flush() + return group + + + + def user_set_default_access( self, user, groups = None, roles = None, history = False, dataset = False ): + if groups is None and roles is None: + groups = [ self.get_public_group(), self.create_private_user_group( user ) ] + roles = [] + 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: + assoc = self.model.DefaultUserGroupAssociation( user, group ) + assoc.flush() + if roles is not None: + for assoc in user.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = self.model.DefaultUserRoleAssociation( user, role ) + assoc.flush() + if history: + for history in user.histories: + self.history_set_default_access( history, groups = groups, roles = roles, dataset = dataset ) + + def history_set_default_access( self, history, groups = None, roles = None, dataset = False ): + if groups is None and roles is None: + if history.user: + groups = history.user.default_groups + roles = history.user.default_roles + else: + groups = [ self.get_public_group() ] + roles = [] + 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: + assoc = self.model.DefaultHistoryGroupAssociation( history, group ) + assoc.flush() + if roles is not None: + for assoc in history.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = self.model.DefaultHistoryRoleAssociation( history, role ) + 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() ] ) + self.set_dataset_roles( data.dataset, [] ) + break + else: + self.set_dataset_groups( data.dataset, groups ) + self.set_dataset_roles( data.dataset, roles ) + + def get_public_group( self ): + return self.model.Group.get_public_group() + + def set_dataset_groups( self, dataset, groups ): + if isinstance( dataset, self.model.HistoryDatasetAssociation): + dataset = dataset.dataset + for assoc in dataset.groups: + assoc.delete() + assoc.flush() + for group in groups: + if not isinstance( group, self.model.Group ): + group = group.group + self.associate_components( dataset = dataset, group = group ) + def set_dataset_roles( self, dataset, roles ): + if isinstance( dataset, self.model.HistoryDatasetAssociation): + dataset = dataset.dataset + for assoc in dataset.roles: + assoc.delete() + assoc.flush() + for role in roles: + if not isinstance( role, self.model.Role ): + role = role.role + self.associate_components( dataset = dataset, role = role ) + + + def get_component_associations( self, **kwd ): + 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 'role' in kwd: + return self.model.RoleDatasetAssociation.get_by( role_id = kwd['role'].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 ) + elif 'role' in kwd: + return self.model.UserRoleAssociation.get_by( user_id = kwd['user'].id, role_id = kwd['role'].id ) + elif 'role' in kwd: + if 'group' in kwd: + return self.model.GroupRoleAssociation.get_by( group_id = kwd['group'].id, role_id = kwd['role'].id ) + elif 'control_role' in kwd: + return self.model.RoleControlRoleAssociation.get_by( target_role_id = kwd['role'].id, role_id = kwd['control_role'].id ) + elif 'target_role' in kwd: + return self.model.RoleControlRoleAssociation.get_by( role_id = kwd['role'].id, target_role_id = kwd['target_role'].id ) + elif 'group' in kwd: + if 'control_role' in kwd: + return self.model.GroupControlRoleAssociation.get_by( group_id = kwd['group'].id, role_id = kwd['control_role'].id ) + raise 'No valid method of associating provided components: %s' % kwd + + + + + + diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index af94a3a24ce..4f646b7b6d3 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,8 +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 ) - child_dataset.dataset.set_groups( outdata.dataset.groups ) - child_dataset.dataset.set_roles( outdata.dataset.roles ) + self.app.security_agent.set_dataset_groups( child_dataset.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_roles( child_dataset.dataset, outdata.dataset.roles ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1123,8 +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 ) - primary_data.dataset.set_groups( outdata.dataset.groups ) - primary_data.dataset.set_roles( outdata.dataset.roles ) + self.app.security_agent.set_dataset_groups( primary_data.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_roles( primary_data.dataset, outdata.dataset.roles ) 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 25393b29b7f..e3e042fd71c 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -43,7 +43,7 @@ class DefaultToolAction( object ): assoc.flush() data = new_data break - if data and not data.allow_action( trans.user, data.access_actions.USE ): + if data and not trans.app.security_agent.allow_action( trans.user, data.access_actions.USE, dataset = data ): raise "User does not have permission to use a dataset (%s) provided for input." % data.id return data if isinstance( input, DataToolParameter ): @@ -85,7 +85,7 @@ class DefaultToolAction( object ): #determine output dataset access list existing_datasets = [ inp for inp in inp_data.values() if inp ] if existing_datasets: - output_access_groups, output_access_roles = existing_datasets[0].dataset.guess_derived_groups_roles( existing_datasets[1:] ) + output_access_groups, output_access_roles = trans.app.security_agent.guess_derived_groups_roles_for_datasets( existing_datasets ) else: #no valid inputs, we will use history defaults output_access_roles = [ role.role for role in trans.history.default_roles ] @@ -130,10 +130,10 @@ class DefaultToolAction( object ): if ext == "input": ext = input_ext data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) - data.dataset.set_groups( output_access_groups ) - data.dataset.set_roles( output_access_roles ) # 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 ) + trans.app.security_agent.set_dataset_roles( data.dataset, output_access_roles ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations @@ -197,7 +197,7 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: - if not dataset.allow_action( trans.user, dataset.access_actions.USE ): + if not trans.app.security_agent.allow_action( trans.user, dataset.access_actions.USE, 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: diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 6d7675a9456..195fb3ef884 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -66,9 +66,9 @@ class UploadToolAction( object ): def upload_empty(self, trans, err_code, err_msg): data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) - data.dataset.set_groups( trans.history.default_groups ) - data.dataset.set_roles( trans.history.default_roles ) - data.name = err_code + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_roles( data.dataset, trans.history.default_roles ) + data.name = err_code data.extension = "txt" data.dbkey = "?" data.info = err_msg @@ -161,8 +161,8 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) - data.dataset.set_groups( trans.history.default_groups ) - data.dataset.set_roles( trans.history.default_roles ) + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_roles( data.dataset, trans.history.default_roles ) 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 379529b8cab..5c598e65406 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -981,27 +981,30 @@ class DataToolParameter( ToolParameter ): >>> # Mock up a history (not connected to database) >>> from galaxy.model import History, HistoryDatasetAssociation, User, Role, Permission, Group, GroupRoleAssociation >>> from galaxy.util.bunch import Bunch + >>> from galaxy.security import GalaxyRBACAgent + >>> import galaxy.model + >>> security_agent = GalaxyRBACAgent( galaxy.model ) >>> hist = History() >>> hist.flush() >>> permission = Permission( 'test', list( Permission.dataset_actions.__dict__.values() ) ) >>> permission.flush() >>> role = Role( 'test' ) >>> role.flush() - >>> assoc = role.add_permission( permission ) + >>> assoc = security_agent.associate_components( role = role, permission = permission ) >>> group = Group( 'test' ) >>> group.flush() >>> Group.public_id = group.id - >>> GroupRoleAssociation( group, role ).flush() + >>> assoc = security_agent.associate_components( group = group, role = role ) >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) - >>> dataset1.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset1, [ group ] ) >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) - >>> dataset2.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset2, [ group ] ) >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) - >>> dataset3.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset3, [ group ] ) >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) - >>> dataset4.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset4, [ group ] ) >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) - >>> dataset5.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset5, [ group ] ) >>> hist.add_dataset( dataset1 ) >>> hist.add_dataset( dataset2 ) >>> hist.add_dataset( dataset3 ) @@ -1010,7 +1013,7 @@ class DataToolParameter( ToolParameter ): >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah - >>> print p.get_html( trans=Bunch( history=hist, user=None ) ) + >>> print p.get_html( trans=Bunch( history=hist, user=None, app=Bunch( security_agent = security_agent ) ) ) diff --git a/templates/history/options.mako b/templates/history/options.mako index 1bb2794a32b..4bebc932dca 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,7 +18,7 @@ %endif %if app.config.enable_beta_features:
  • Construct workflow from the current history
  • -
  • Change default permissions for 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 index 315609189cd..6607f002c41 100644 --- a/templates/history/permissions.mako +++ b/templates/history/permissions.mako @@ -1,11 +1,11 @@ <%inherit file="/base.mako"/> -<%def name="title()">Change Default History Permissions +<%def name="title()">Change Default History Permitted Actions %if trans.user:
    -
    Change Default History Permissions
    +
    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 ] %> @@ -29,12 +29,12 @@
    - This will change the default permissions assigned to new datasets for your current history. + This will change the default permitted actions assigned to new datasets for your current history.
    - +
    diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index d59e8cdcf17..607745ab20b 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -32,7 +32,7 @@ ## Body for history items, extra info and actions, data "peek"
    - %if not trans.app.security_agent.allow_action( trans.user, data.access_actions.VIEW, dataset = data.dataset ): + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data.dataset ):
    You do not have permision to view this dataset.
    %elif data_state == "queued":
    Job is waiting to run
    diff --git a/templates/user/index.mako b/templates/user/index.mako index 64d23dd2410..1047466aad0 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -9,7 +9,7 @@
  • Change your password
  • Update your email address
  • %if app.config.enable_beta_features: -
  • Change default permissions for new histories
  • +
  • Change default permitted actions for new histories
  • %endif
  • Logout
  • diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako index 1b0803d44df..7b6b90f976d 100644 --- a/templates/user/permissions.mako +++ b/templates/user/permissions.mako @@ -1,11 +1,11 @@ <%inherit file="/base.mako"/> -<%def name="title()">Change Default History Permissions +<%def name="title()">Change Default History Permitted Actions %if trans.user:
    -
    Change Default Permissions for new Histories
    +
    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 ] %> @@ -29,12 +29,12 @@
    - This will change the default permissions assigned to new datasets for new histories. + This will change the default permitted actions assigned to new datasets for new histories.
    - +
    diff --git a/tools/data_source/encode_import_code.py b/tools/data_source/encode_import_code.py index 6a706b5ab04..ddfbb0241e0 100644 --- a/tools/data_source/encode_import_code.py +++ b/tools/data_source/encode_import_code.py @@ -38,8 +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.security_agent.set_dataset_roles( newdata.dataset, base_dataset.dataset.roles ) 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 ad6a877e353..e8816f093ff 100644 --- a/tools/data_source/microbial_import_code.py +++ b/tools/data_source/microbial_import_code.py @@ -129,8 +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 ) - app.security_agent.set_dataset_roles( newdata.dataset, base_dataset.dataset.roles ) 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 d3784661188..428486b3e37 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -32,8 +32,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr 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 ) - app.security_agent.set_dataset_roles( newdata.dataset, output_data.dataset.roles ) newdata.flush() history.flush() app.model.flush() From dc9ffe68605089a3fe1e0684859c7d689a20036c Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Mon, 11 Aug 2008 16:58:48 -0400 Subject: [PATCH 10/94] Actions have been reduced to 3, and the public group is now the one named 'public' (names, for now, will be unique). --- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/security/__init__.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 58a4f596104..cadd7a78ef0 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -144,7 +144,7 @@ class Group( object ): def guess_public_group( cls ): # TODO, Nate: Make sure this method is functionally correct. #retrieve from database and store public group id, assume first created group is public - cls.set_public_group( Group.select( order_by = Group.table.c.create_time )[0] ) + cls.set_public_group( Group.select_by( name = 'public' ) ) class UserGroupAssociation( object ): def __init__( self, user, group ): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index ebccb02e005..6582da8c368 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -14,17 +14,9 @@ log = logging.getLogger(__name__) class RBACAgent: """Class that handles galaxy security""" permitted_actions = Bunch( - dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading - USE = 'dataset_use', #use in jobs - ADD_GROUP = 'dataset_add_group', #dataset can be added to groups - REMOVE_GROUP = 'dataset_remove_group' #dataset can be removed from groups - ), - group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add dataset to group - REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group - DELETE = 'group_delete', #delete a group - ADD_USER = 'group_add_user', #add users to group - REMOVE_USER = 'group_remove_user' #remove user from group - ) + EDIT_METADATA = 'edit_metadata', + MANAGE_PERMISSIONS = 'manage_permissions', + ACCESS = 'access' ) def allow_action( self, user, action, **kwd ): raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) From df6ab911b103db183ba3d270e83a58f3b1d504d8 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 12 Aug 2008 14:19:45 -0400 Subject: [PATCH 11/94] Fix references to actions removed in a previous commit. --- lib/galaxy/model/__init__.py | 6 ++---- lib/galaxy/security/__init__.py | 19 +++++++++++++++---- lib/galaxy/tools/actions/__init__.py | 4 ++-- lib/galaxy/tools/parameters/basic.py | 4 ++-- lib/galaxy/web/controllers/dataset.py | 4 ++-- lib/galaxy/web/controllers/root.py | 11 +++++++---- templates/dataset/edit_attributes.mako | 2 ++ templates/root/history_common.mako | 4 ++-- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index cadd7a78ef0..c47235036a0 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -104,8 +104,6 @@ class JobToOutputDatasetAssociation( object ): self.dataset = dataset class GroupDatasetAssociation( object ): - dataset_actions = RBACAgent.permitted_actions.dataset_actions - group_actions = RBACAgent.permitted_actions.group_actions def __init__( self, group, dataset, permitted_actions=[] ): if isinstance( group, GroupDatasetAssociation ) or \ isinstance( group, DefaultUserGroupAssociation ) or \ @@ -125,7 +123,7 @@ class GroupDatasetAssociation( object ): class Group( object ): public_id = None - permitted_actions = GroupDatasetAssociation.group_actions + permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) def __init__( self, name = None, priority = 0 ): self.name = name self.priority = priority @@ -179,7 +177,7 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) - permitted_actions = GroupDatasetAssociation.dataset_actions + 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 ): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 6582da8c368..94315714c4f 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -14,9 +14,9 @@ log = logging.getLogger(__name__) class RBACAgent: """Class that handles galaxy security""" permitted_actions = Bunch( - EDIT_METADATA = 'edit_metadata', - MANAGE_PERMISSIONS = 'manage_permissions', - ACCESS = 'access' + DATASET_EDIT_METADATA = 'dataset_edit_metadata', + DATASET_MANAGE_PERMISSIONS = 'dataset_manage_permissions', + 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 ) @@ -68,7 +68,7 @@ class GalaxyRBACAgent( RBACAgent ): 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 in [ self.permitted_actions.dataset_actions.USE, self.permitted_actions.dataset_actions.VIEW ] and \ + 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: @@ -229,3 +229,14 @@ class GalaxyRBACAgent( RBACAgent ): 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 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/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index deb54fe6a0a..bd74d188ee6 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -44,7 +44,7 @@ class DefaultToolAction( object ): 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.USE, dataset=data ): + 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 ): @@ -197,7 +197,7 @@ class DefaultToolAction( object ): 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.USE, dataset=dataset ): + 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: diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index d889d9dd79d..b626e665472 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -1065,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 and trans.app.security_agent.allow_action( trans.user, data.permitted_actions.USE, dataset = data ): + 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): @@ -1079,7 +1079,7 @@ 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.USE, dataset = data ): + 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 ) diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index 0f2f52dcc01..d38537509cd 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -107,7 +107,7 @@ class DatasetInterface( BaseController ): if not data: raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) # TODO, Nate: Make sure the following is functionally correct. - if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data ): + 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) @@ -127,4 +127,4 @@ class DatasetInterface( BaseController ): except: raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: - raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." ) \ No newline at end of file + raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index e6328ba3c8a..8841c46eedd 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -139,7 +139,7 @@ class RootController( BaseController ): except: return "Dataset id '%s' is invalid" %str( id ) if data: - if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data ): + 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: @@ -171,7 +171,7 @@ class RootController( BaseController ): if data: child = data.get_child_by_designation( designation ) if child: - if trans.app.security_agent.allow_action( trans.user, child.permitted_actions.VIEW, dataset = child ): + 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." @@ -184,7 +184,7 @@ 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: - if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data ): + 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 ) @@ -219,7 +219,7 @@ class RootController( BaseController ): return trans.show_error_message( "Problem retrieving dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) 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.USE, dataset = data ): + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): p = util.Params(kwd, safe=False) if p.change: @@ -262,6 +262,8 @@ class RootController( BaseController ): if target_type: msg = data.datatype.convert_dataset(trans, data, target_type) return trans.show_ok_message( msg, refresh_frames=['history'] ) + ''' + # Users can't currently change permissions or groups elif p.change_permision: """The user clicked the change_permision button on the 'Change permissions' form""" if not trans.user: @@ -283,6 +285,7 @@ class RootController( BaseController ): 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 ) diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 314b7960e78..1479b15c20a 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -133,6 +133,7 @@

    +<%doc> %if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.REMOVE_GROUP, dataset = data ) or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.ADD_GROUP, dataset = data ) ):

    Change Permitted Actions
    @@ -163,3 +164,4 @@
    %endif + diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 607745ab20b..aa9a5c238a5 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -32,7 +32,7 @@ ## Body for history items, extra info and actions, data "peek"
    - %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data.dataset ): + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ):
    You do not have permision to view this dataset.
    %elif data_state == "queued":
    Job is waiting to run
    @@ -102,4 +102,4 @@
    - \ No newline at end of file + From d4474cd22fb0184c83f1d3e6180f3eed4fd68f6e Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 12 Aug 2008 14:53:22 -0400 Subject: [PATCH 12/94] Change back the way we guess the public group (as per Greg) and add a deleted column to Group so groups can be undeleted. ALTER TABLE galaxy_group ADD COLUMN deleted BOOLEAN; --- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/model/mapping.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index c47235036a0..7dda26375bb 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -142,7 +142,7 @@ class Group( object ): def guess_public_group( cls ): # TODO, Nate: Make sure this method is functionally correct. #retrieve from database and store public group id, assume first created group is public - cls.set_public_group( Group.select_by( name = 'public' ) ) + cls.set_public_group( Group.select( order_by = Group.table.c.create_time )[0] ) class UserGroupAssociation( object ): def __init__( self, user, group ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index c99899d4e5a..8c11ec41a2c 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -120,7 +120,8 @@ Group.table = Table( "galaxy_group", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "name", TEXT ), - Column( "priority", Integer ) ) + Column( "priority", Integer ), + Column( "deleted", Boolean, index=True, default=False ) ) UserGroupAssociation.table = Table( "user_group_association", metadata, Column( "id", Integer, primary_key=True ), From da0b09039cdb013e0ad15c325bc500abf003f7e3 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Tue, 12 Aug 2008 16:56:26 -0400 Subject: [PATCH 13/94] Initial check in of library functionality. The ui is Very rough, at best. Libraries can be accessed by users through a new tool under datasources. Libraries can be created and accessed for modification via a link on the admin page. Currently, all library datasets are currently associated with the public group. Tags have been defined (tables and mappings) and can be associated with a dataset or a folder, but there is currenty no interface for displaying or creating these. Creation and editing of a library should be an admin only function, however this check is not currently present. The creation/editing page for a dataset is currently hard coded, it should be dynamic (i.e. datatypes and dbkeys) Brief description of a library: A library is composed of a name, description and a root folder. A folder is composed of a name, description, and a parent folder (none when is the root), where a folder can have many or none child folders and datasets. A library dataset is composed of most of the same attributes as a history dataset, but it is linked via a folder_id to a folder (instead of a history_id to a history). Ordering of the library is maintained by the 'order_id' attribute of each folder and dataset object (which is populated by the item_count attribute of a folder) --- lib/galaxy/model/__init__.py | 123 ++- lib/galaxy/model/mapping.py | 118 ++- lib/galaxy/security/__init__.py | 2 +- lib/galaxy/web/controllers/library.py | 218 +++++ lib/galaxy/web/framework/__init__.py | 7 +- templates/admin_main.mako | 6 + templates/form.mako | 2 + templates/library/admin_list_libraries.mako | 16 + templates/library/manage_dataset.mako | 100 ++ templates/library/manage_folder.mako | 191 ++++ templates/library/manage_library.mako | 46 + templates/library/new_dataset.mako | 997 ++++++++++++++++++++ templates/library/user_list_libraries.mako | 14 + templates/library/user_view_library.mako | 58 ++ tool_conf.xml.sample | 1 + tools/data_source/access_libraries.xml | 11 + 16 files changed, 1883 insertions(+), 27 deletions(-) create mode 100644 lib/galaxy/web/controllers/library.py create mode 100644 templates/library/admin_list_libraries.mako create mode 100644 templates/library/manage_dataset.mako create mode 100644 templates/library/manage_folder.mako create mode 100644 templates/library/manage_library.mako create mode 100644 templates/library/new_dataset.mako create mode 100644 templates/library/user_list_libraries.mako create mode 100644 templates/library/user_view_library.mako create mode 100644 tools/data_source/access_libraries.xml diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 7dda26375bb..e6637b2ad7f 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -262,16 +262,16 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) -class HistoryDatasetAssociation( object ): + +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, copied_from_history_dataset_association = None, validation_errors=None, - visible=True, create_dataset = False ): + parent_id=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 @@ -282,14 +282,12 @@ class HistoryDatasetAssociation( object ): self.deleted = deleted self.visible = visible # Relationships - self.history = history if not dataset and create_dataset: dataset = Dataset() dataset.flush() 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 ): @@ -399,10 +397,7 @@ class HistoryDatasetAssociation( object ): 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 ) + raise 'Unimplemented' def get_child_by_designation(self, designation): for child in self.children: if child.designation == designation: @@ -411,17 +406,6 @@ class HistoryDatasetAssociation( object ): def get_converter_types(self): return self.datatype.get_converter_types( self, datatypes_registry) - - def copy( self, copy_children = False, parent_id = None, target_user = None ): - if target_user is None: target_user = self.user - 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 ) @@ -436,6 +420,35 @@ class HistoryDatasetAssociation( object ): 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 ): + print "self.dataset", self.dataset + + 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 ) + print "des data", des.dataset + 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 @@ -499,9 +512,75 @@ class History( object ): 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, order_id = None ): + self.name = name or "Unnamed folder" + self.description = description + self.item_count = item_count + self.order_id = order_id + def add_dataset( self, dataset ): + dataset.folder_id = self.id + dataset.order_id = self.item_count + self.item_count += 1 + 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" diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 8c11ec41a2c..6a8f982fc81 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -74,6 +74,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 ) ), @@ -157,6 +158,66 @@ DefaultHistoryGroupAssociation.table = Table( "default_history_group_association 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 ) ) + +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 ), @@ -298,6 +359,10 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), @@ -311,7 +376,10 @@ assign_mapper( context, Dataset, Dataset.table, properties=dict( history_associations=relation( HistoryDatasetAssociation, - primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ) + primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ), + library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) ) ) ) @@ -365,6 +433,54 @@ assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssoc 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 ) ) ) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 94315714c4f..0ba0bf6b621 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -1,5 +1,5 @@ """ -Utility functions used systemwide. +Galaxy Security """ import logging diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py new file mode 100644 index 00000000000..c8b1f6f0573 --- /dev/null +++ b/lib/galaxy/web/controllers/library.py @@ -0,0 +1,218 @@ + +from galaxy.web.base.controller import * +from galaxy.datatypes import sniff +import logging, shutil, StringIO + +log = logging.getLogger( __name__ ) + +class Library( BaseController ): + + @web.expose + def index( self, trans, library_id = None, import_ids = [], **kwd ): + #use for importing an entry into your history + if import_ids: + 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: + return trans.fill_template( 'library/user_view_library.mako', library = trans.app.model.Library.get( library_id ) ) + return trans.fill_template( 'library/user_list_libraries.mako', libraries = trans.app.model.Library.select() ) + + #make admin only + @web.expose + def manage_libraries( self, trans, **kwd ): + return trans.fill_template( 'library/admin_list_libraries.mako', libraries = trans.app.model.Library.select() ) + + #make admin only + @web.expose + def manage_library( self, trans, id=None, name="Unnamed", description=None, **kwd ): + 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='manage_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( 'library/manage_library.mako', library = library ) + else: + return trans.show_error_message( "Invalid library specified" ) + + #make admin only + @web.expose + def manage_folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): + if 'create_folder' in kwd: + folder = trans.app.model.LibraryFolder( name = name, description = description ) + 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='manage_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( 'library/manage_folder.mako', folder = folder, msg = msg ) + else: + return trans.show_error_message( "Invalid folder specified" ) + + + #make admin only + @web.expose + def manage_dataset( self, trans, id=None, name="Unnamed", info = 'no info', extension = None, folder_id = None, dbkey = None, **kwd ): + data_files = [] + def add_file( file_obj, name, extension, dbkey, 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 ) + dataset.flush() + # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public + trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) + 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 + 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 + 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, 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, 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, info="pasted entry", space_to_tab=space_to_tab ) + trans.response.send_redirect( web.url_for( action='manage_dataset', id = last_dataset_created.id ) ) + #return self.manage_dataset( trans, id = last_dataset_created.id ) + elif id is None: + return trans.fill_template( 'library/new_dataset.mako', folder_id = folder_id ) + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if dataset: + #copied from edit attributes for 'regular' datasets + 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( 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 ) + + 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( "/library/manage_dataset.mako", dataset=dataset, metadata=metadata, + datatypes=ldatatypes, err=None ) + else: + return trans.show_error_message( "Invalid dataset specified" ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 529cb6156b0..124d1549453 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -527,8 +527,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 ) @@ -539,13 +539,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_main.mako b/templates/admin_main.mako index f9c26121124..02e39576685 100644 --- a/templates/admin_main.mako +++ b/templates/admin_main.mako @@ -29,5 +29,11 @@ + + + Manage Libraries + + + diff --git a/templates/form.mako b/templates/form.mako index c003de02585..42fdb745ba3 100644 --- a/templates/form.mako +++ b/templates/form.mako @@ -21,9 +21,11 @@ $(function(){ cls += " form-row-error" %>
    + %if input.use_label: + %endif
    diff --git a/templates/library/admin_list_libraries.mako b/templates/library/admin_list_libraries.mako new file mode 100644 index 00000000000..ffe82787da2 --- /dev/null +++ b/templates/library/admin_list_libraries.mako @@ -0,0 +1,16 @@ +<%inherit file="/base.mako"/> +<%def name="title()">View Libraries + +
    +
    Manage Libraries
    +
    + %for library in libraries: + + %endfor + +
    +
    diff --git a/templates/library/manage_dataset.mako b/templates/library/manage_dataset.mako new file mode 100644 index 00000000000..f04615def51 --- /dev/null +++ b/templates/library/manage_dataset.mako @@ -0,0 +1,100 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Edit Dataset Attributes + + +<%def name="datatype( dataset, datatypes )"> + + + +
    +
    Edit Attributes
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + %for element in metadata: +
    + +
    + ${element.get_html()} +
    +
    +
    + %endfor +
    + +
    +
    +
    + +
    + +
    +
    + This will inspect the dataset and attempt to correct the above column values + if they are not accurate. +
    +
    +
    +
    + +

    + + +

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

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

    +
    +
    ${data.display_name()}
    +
    +
    + + ## Header row for history items (name, state, action buttons) + +
    + %if data_state != 'ok': +
    + %endif +
    +
    + ##display data + edit attributes + ##delete +
    + ##${data.display_name()} +
    + + ## Body for history items, extra info and actions, data "peek" + +
    + %if data_state == "queued": +
    Job is waiting to run
    + %elif data_state == "running": +
    Job is currently running
    + %elif data_state == "error": +
    + An error occurred running this job: ${data.display_info().strip()}, + report this error +
    + %elif data_state == "empty": +
    No data: ${data.display_info()}
    + %elif data_state == "ok": +
    + ${data.blurb}, + format: ${data.ext}, + database: + %if data.dbkey == '?': + ${data.dbkey} + %else: + ${data.dbkey} + %endif +
    +
    Info: ${data.display_info()}
    + %if data.peek != "no peek": +
    ${data.display_peek()}
    + %endif + %else: +
    Error: unknown dataset state "${data_state}".
    + %endif + + ## Recurse for child datasets + + +
    +
    +
    + + +## Render a folder +<%def name="render_folder( this_folder )"> + +
    +
    Contents of Folder: ${this_folder.name}
    +
    +
    +
    + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> + %for component in components: + ${render_component( component )} + %endfor + +
    +
    +
    + +
    + +
    +
    + + + + + + +<%def name="title()">Manage Folder: ${folder.name} + +
    +
    Change Folder Attributes
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + + + +
    + +
    +
    Manage Folder Contents: ${folder.name}
    +
    +
    + %if folder.parent: + up a level + %elif folder.library_root: + manage library + %endif +
    +
    +
    + ${render_folder( folder )} +
    + +
    + +
    + +
    +
    + + diff --git a/templates/library/manage_library.mako b/templates/library/manage_library.mako new file mode 100644 index 00000000000..5d94dfb6b78 --- /dev/null +++ b/templates/library/manage_library.mako @@ -0,0 +1,46 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Manage Library + + +
    +
    Edit a Library: ${library.name}
    +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + diff --git a/templates/library/new_dataset.mako b/templates/library/new_dataset.mako new file mode 100644 index 00000000000..1c517cdfede --- /dev/null +++ b/templates/library/new_dataset.mako @@ -0,0 +1,997 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Create New Library Dataset + +
    +
    Create a new Library Dataset
    +
    +
    + + + +
    + + +
    + + +
    + + +
    + +
    + + +
    + +
    + Here you may specify a list of URLs (one per line) or paste the contents of a file. +
    + + + +
    + +
    + +
    + + +
    Yes
    + +
    + + Use this option if you are entering intervals by hand. +
    + + +
    + +
    + +
    + + +
    + +
    + Which format? See help below +
    + + +
    + +
    + +
    + + +##this should be generated dynamically +
    + + + +
    + +
    + + +
    + +
    + + +
    +
    +
    + diff --git a/templates/library/user_list_libraries.mako b/templates/library/user_list_libraries.mako new file mode 100644 index 00000000000..23a2b3b9ad1 --- /dev/null +++ b/templates/library/user_list_libraries.mako @@ -0,0 +1,14 @@ +<%inherit file="/base.mako"/> +<%def name="title()">View Libraries + +
    +
    View Library
    +
    + %for library in libraries: + + %endfor + +
    +
    diff --git a/templates/library/user_view_library.mako b/templates/library/user_view_library.mako new file mode 100644 index 00000000000..3b948558f28 --- /dev/null +++ b/templates/library/user_view_library.mako @@ -0,0 +1,58 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + return render_dataset( component ) + %> + + + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> +
    + ${data.name} +
    + + +## Render a folder +<%def name="render_folder( this_folder )"> + +
    + Folder: ${this_folder.name} + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> +
    + %for component in components: + ${render_component( component )} + %endfor +
    +
    + + + + + + + +<%def name="title()">View Library: ${library.name} + +
    +
    Import from Library: ${library.name}
    +
    +
    + ${render_folder( library.root_folder )} + +
    + +
    + +
    +
    + diff --git a/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..ffd383c481a --- /dev/null +++ b/tools/data_source/access_libraries.xml @@ -0,0 +1,11 @@ + + + + stored locally + + + + + + + From f0396dbc188aa381e9fa2d953900583e08e010ad Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 14 Aug 2008 16:18:06 -0400 Subject: [PATCH 14/94] First pass at administration components that incorporate dataset security and libraries. --- lib/galaxy/config.py | 2 +- lib/galaxy/model/__init__.py | 15 +- lib/galaxy/model/mapping.py | 12 +- lib/galaxy/security/__init__.py | 7 +- lib/galaxy/web/controllers/admin.py | 680 +++++++++++- lib/galaxy/web/controllers/library.py | 201 +--- .../admin/dataset_security/group_create.mako | 96 ++ .../group_dataset_permitted_actions_edit.mako | 71 ++ .../admin/dataset_security/group_members.mako | 47 + .../dataset_security/group_members_edit.mako | 114 ++ templates/admin/dataset_security/groups.mako | 67 ++ templates/admin/dataset_security/index.mako | 13 + .../specified_users_groups.mako | 56 + templates/admin/dataset_security/users.mako | 80 ++ templates/admin/index.mako | 13 + templates/admin/library/dataset.mako | 85 ++ templates/admin/library/folder.mako | 158 +++ templates/admin/library/libraries.mako | 19 + templates/admin/library/library.mako | 35 + templates/admin/library/new_dataset.mako | 953 +++++++++++++++++ templates/admin/reload_tool.mako | 28 + templates/admin_main.mako | 39 - templates/library/admin_list_libraries.mako | 16 - templates/library/libraries.mako | 13 + .../{user_view_library.mako => library.mako} | 52 +- templates/library/manage_dataset.mako | 100 -- templates/library/manage_folder.mako | 191 ---- templates/library/manage_library.mako | 46 - templates/library/new_dataset.mako | 997 ------------------ templates/library/user_list_libraries.mako | 14 - templates/root/masthead.mako | 3 + universe_wsgi.ini.sample | 4 +- 32 files changed, 2557 insertions(+), 1670 deletions(-) create mode 100644 templates/admin/dataset_security/group_create.mako create mode 100644 templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako create mode 100644 templates/admin/dataset_security/group_members.mako create mode 100644 templates/admin/dataset_security/group_members_edit.mako create mode 100644 templates/admin/dataset_security/groups.mako create mode 100644 templates/admin/dataset_security/index.mako create mode 100644 templates/admin/dataset_security/specified_users_groups.mako create mode 100644 templates/admin/dataset_security/users.mako create mode 100644 templates/admin/index.mako create mode 100644 templates/admin/library/dataset.mako create mode 100644 templates/admin/library/folder.mako create mode 100644 templates/admin/library/libraries.mako create mode 100644 templates/admin/library/library.mako create mode 100644 templates/admin/library/new_dataset.mako create mode 100644 templates/admin/reload_tool.mako delete mode 100644 templates/admin_main.mako delete mode 100644 templates/library/admin_list_libraries.mako create mode 100644 templates/library/libraries.mako rename templates/library/{user_view_library.mako => library.mako} (58%) delete mode 100644 templates/library/manage_dataset.mako delete mode 100644 templates/library/manage_folder.mako delete mode 100644 templates/library/manage_library.mako delete mode 100644 templates/library/new_dataset.mako delete mode 100644 templates/library/user_list_libraries.mako 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/model/__init__.py b/lib/galaxy/model/__init__.py index e6637b2ad7f..460b6433b1d 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -129,20 +129,18 @@ class Group( object ): self.priority = priority @classmethod def get_public_group( cls ): - # TODO, Nate: Make sure this method is functionally correct. return Group.get( cls.public_id ) @classmethod def set_public_group( cls, group ): - # TODO, Nate: Make sure this method is functionally correct. - #we store the id instead of the object, because of alchemy sessions + # 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 ): - # TODO, Nate: Make sure this method is functionally correct. - #retrieve from database and store public group id, assume first created group is public - cls.set_public_group( Group.select( order_by = Group.table.c.create_time )[0] ) + # Retrieve from database and store public group id + group = Group.select_by( name='public' )[0] + cls.set_public_group( group ) class UserGroupAssociation( object ): def __init__( self, user, group ): @@ -262,7 +260,6 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) - class DatasetInstance( object ): """A base class for all 'dataset instances', HDAs, LDAs, etc""" states = Dataset.states @@ -419,8 +416,6 @@ class DatasetInstance( object ): 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 ) @@ -521,7 +516,7 @@ class Library( object ): self.root_folder = root_folder class LibraryFolder( object ): - def __init__( self, name = None, description = None, order_id = None ): + 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 diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 6a8f982fc81..e6f132693da 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -139,9 +139,7 @@ GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "permitted_actions", JSONType(), default=[] ) ) -# TODO, Nate: Need to better understand what these Default tables are for and add appropriate -# comments here to clarify them. Need to ensure that they should include the permitted_actions -# columns, and if so, that they are correctly populated. +# 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 ), @@ -150,6 +148,8 @@ DefaultUserGroupAssociation.table = Table( "default_user_group_association", met 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 ), @@ -417,10 +417,6 @@ assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, properties=dict( user=relation( User, backref = "groups" ), group=relation( Group, backref = "users" ) ) ) - -# TODO, Nate: Need to make sure we have optimal performance - may need more mappers... -# if we have a user and a list of datasets, what is the fastest -# way to ask whether the user has a certain action on all of them. assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "groups" ), group=relation( Group, backref = "datasets" ) ) ) @@ -609,7 +605,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): 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' ) + 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 diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 0ba0bf6b621..ed73ec2364e 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -13,9 +13,14 @@ log = logging.getLogger(__name__) # are correct when an authenticated user creates things inside their "private" environment. class RBACAgent: """Class that handles galaxy security""" - permitted_actions = Bunch( + 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 ): diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index fd9b4be90d8..3367ce6bc36 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,24 +1,676 @@ +import shutil, StringIO 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 ) + 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 ) + data_files = [] + def add_file( file_obj, name, extension, dbkey, 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 ) + dataset.flush() + # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public + trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) + 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 + 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 + 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, 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, 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, info="pasted entry", space_to_tab=space_to_tab ) + trans.response.send_redirect( web.url_for( action='dataset', id = last_dataset_created.id ) ) + #return self.dataset( trans, id = last_dataset_created.id ) + elif id is None: + return trans.fill_template( '/admin/library/new_dataset.mako', folder_id = folder_id ) + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if dataset: + #copied from edit attributes for 'regular' datasets + 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( 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 ) + + 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, + err=None ) + else: + return trans.show_error_message( "Invalid dataset specified" ) diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index c8b1f6f0573..94e7b5731e0 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -1,12 +1,10 @@ from galaxy.web.base.controller import * -from galaxy.datatypes import sniff -import logging, shutil, StringIO +import logging log = logging.getLogger( __name__ ) class Library( BaseController ): - @web.expose def index( self, trans, library_id = None, import_ids = [], **kwd ): #use for importing an entry into your history @@ -21,198 +19,5 @@ class Library( BaseController ): history.flush() return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) elif library_id: - return trans.fill_template( 'library/user_view_library.mako', library = trans.app.model.Library.get( library_id ) ) - return trans.fill_template( 'library/user_list_libraries.mako', libraries = trans.app.model.Library.select() ) - - #make admin only - @web.expose - def manage_libraries( self, trans, **kwd ): - return trans.fill_template( 'library/admin_list_libraries.mako', libraries = trans.app.model.Library.select() ) - - #make admin only - @web.expose - def manage_library( self, trans, id=None, name="Unnamed", description=None, **kwd ): - 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='manage_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( 'library/manage_library.mako', library = library ) - else: - return trans.show_error_message( "Invalid library specified" ) - - #make admin only - @web.expose - def manage_folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): - if 'create_folder' in kwd: - folder = trans.app.model.LibraryFolder( name = name, description = description ) - 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='manage_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( 'library/manage_folder.mako', folder = folder, msg = msg ) - else: - return trans.show_error_message( "Invalid folder specified" ) - - - #make admin only - @web.expose - def manage_dataset( self, trans, id=None, name="Unnamed", info = 'no info', extension = None, folder_id = None, dbkey = None, **kwd ): - data_files = [] - def add_file( file_obj, name, extension, dbkey, 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 ) - dataset.flush() - # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public - trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) - 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 - 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 - 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, 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, 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, info="pasted entry", space_to_tab=space_to_tab ) - trans.response.send_redirect( web.url_for( action='manage_dataset', id = last_dataset_created.id ) ) - #return self.manage_dataset( trans, id = last_dataset_created.id ) - elif id is None: - return trans.fill_template( 'library/new_dataset.mako', folder_id = folder_id ) - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) - if dataset: - #copied from edit attributes for 'regular' datasets - 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( 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 ) - - 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( "/library/manage_dataset.mako", dataset=dataset, metadata=metadata, - datatypes=ldatatypes, err=None ) - else: - return trans.show_error_message( "Invalid dataset specified" ) + return trans.fill_template( '/library/library.mako', library=trans.app.model.Library.get( library_id ) ) + return trans.fill_template( '/library/libraries.mako', libraries=trans.app.model.Library.select() ) 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 +
    +
    + Libraries  |   + Groups  |   + Users +
    +

    Create Group

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

    ${msg}

    + + + + + + %if len( users ) == 0: + + %else: + + + + + + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + + %endif + + +
    Name:   Priority:
    There are no Galaxy users
    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: +
    + %if email.upper().startswith( curr_anchor ): + %if not anchored: +

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

    + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
    +
    +
    diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako new file mode 100644 index 00000000000..58406d7edbf --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,71 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Permitted Actions on Datasets +
    +
    + Libraries  |   + Groups  |   + Users +
    +

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

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

    ${msg}

     
    There is no Galaxy group named '${gn}'
    GroupPriorityPermitted Actions on Datasets
    ${gn}${gda[0]} + %for da in dataset_actions: + <% check = False %> + %for action in gda[1]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
    + %endfor +
    +
    +
    diff --git a/templates/admin/dataset_security/group_members.mako b/templates/admin/dataset_security/group_members.mako new file mode 100644 index 00000000000..36e407959f6 --- /dev/null +++ b/templates/admin/dataset_security/group_members.mako @@ -0,0 +1,47 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Create Group +
    +
    + Libraries  |   + Groups  |   + Users +
    + +
    Members of Group '${gn}'
    + + %if msg: + + %endif + + %if len( members ) == 0: + + %else: + <% ctr = 0 %> + %for member in members: + <% email = unescape( member[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + <% ctr += 1 %> + %endfor + %endif +

    ${msg}

     
    Group '${gn}' contains no members
    ${email}
    +
    diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako new file mode 100644 index 00000000000..c7defaa7697 --- /dev/null +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -0,0 +1,114 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Manage Group Membership +
    +
    + Libraries  |   + Groups  |   + Users +
    + + %if msg: + + %endif + <% gn = unescape( group_name, unentities ) %> + + + + +

    ${msg}

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

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

    + <% + curr_anchor = anchor + anchored = True + %> + %endif + %if check: + ${email} + %else: + ${email} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
    +
    +
    diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako new file mode 100644 index 00000000000..0687802932e --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,67 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Groups +
    +
    + Libraries  |   + Users +
    + +

    Groups

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

    ${msg}

    There are no Galaxy groups
    GroupPriorityMembersDatasetsGroup Permitted Actions on Datasets 
    ${group_name}${group[2]}${group[3]}${group[4]}${group[4]} + %if len( group[5] ) == 1: + ${group[5][0]} + %elif len( group[5] ) > 1: + %for da in group[5]: + ${da}
    + %endfor + %endif +
    Mark group deleted
    +
    diff --git a/templates/admin/dataset_security/index.mako b/templates/admin/dataset_security/index.mako new file mode 100644 index 00000000000..1a013ec1fc6 --- /dev/null +++ b/templates/admin/dataset_security/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Dataset Security +
    +
    Dataset Security
    + + %if msg: + + %endif + + +

    ${msg}

    Groups
    Users
    +
    diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako new file mode 100644 index 00000000000..8a392a67e31 --- /dev/null +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -0,0 +1,56 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% email = unescape( user_email, unentities ) %> + +<%def name="title()">Create Group +
    +
    + Libraries  |   + Groups  |   + Users +
    +

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

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

    ${msg}

    User '${email}' belongs to no groups
    GroupPriorityDatasetsPermitted Actions on Datasets
    ${gn}${group[2]}${group[3]}${group[3]} + %for da in group[4]: + ${da}
    + %endfor +
    +
    diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..22abae62d54 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,80 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Users +
    +
    + Groups  |   + Libraries +
    + + %if msg: + + %endif + + %if len( users ) == 0: + + %else: + + + + + <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% email = unescape( user[1], unentities ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + %endif +

    ${msg}

     
    There are no Galaxy users
    Galaxy Users - Quick Find
    + |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z +
    + %if email.upper().startswith( curr_anchor ): + %if not anchored: +

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

    + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
    +
    diff --git a/templates/admin/index.mako b/templates/admin/index.mako new file mode 100644 index 00000000000..84e90c84b55 --- /dev/null +++ b/templates/admin/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +
    +

    Galaxy Administration

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

    ${msg}

    Dataset Security
    Libraries
    Reload a tool while the Galaxy server is running
    +
    diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako new file mode 100644 index 00000000000..52a35099045 --- /dev/null +++ b/templates/admin/library/dataset.mako @@ -0,0 +1,85 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Edit Dataset Attributes +<%def name="datatype( dataset, datatypes )"> + + +
    +
    Edit Attributes
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + %for element in metadata: +
    + +
    + ${element.get_html()} +
    +
    +
    + %endfor +
    + +
    +
    +
    + +
    + +
    +
    + This will inspect the dataset and attempt to correct the above column values + if they are not accurate. +
    +
    +
    +
    +

    +

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

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

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

    Reload a Tool

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

    ${msg}

    +
    +

    + Reload tool: + + +

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

    Galaxy Administration

    - %if msg: -

    ${msg}

    - %endif -
    -
    -

    Admin password:

    -

    - Reload tool: - - -

    -
    -
    - Manage Libraries -
    - diff --git a/templates/library/admin_list_libraries.mako b/templates/library/admin_list_libraries.mako deleted file mode 100644 index ffe82787da2..00000000000 --- a/templates/library/admin_list_libraries.mako +++ /dev/null @@ -1,16 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">View Libraries - -
    -
    Manage Libraries
    -
    - %for library in libraries: - - %endfor - -
    -
    diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako new file mode 100644 index 00000000000..247333f709c --- /dev/null +++ b/templates/library/libraries.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">View Libraries +
    +
    View Library
    +
    + %for library in libraries: + + %endfor +
    +
    diff --git a/templates/library/user_view_library.mako b/templates/library/library.mako similarity index 58% rename from templates/library/user_view_library.mako rename to templates/library/library.mako index 3b948558f28..2706efcde25 100644 --- a/templates/library/user_view_library.mako +++ b/templates/library/library.mako @@ -1,58 +1,44 @@ <%inherit file="/base.mako"/> <%def name="render_component( component )"> - <% + <% if isinstance( component, trans.app.model.LibraryFolder ): - return render_folder( component ) + return render_folder( component ) elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - return render_dataset( component ) - %> + return render_dataset( component ) + %> - - ## Render the dataset `data` as history item, using `hid` as the displayed id <%def name="render_dataset( data )"> -
    +
    ${data.name} -
    +
    - ## Render a folder <%def name="render_folder( this_folder )"> -
    - Folder: ${this_folder.name} - <% - components = list( this_folder.folders ) + list( this_folder.datasets ) - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> -
    - %for component in components: - ${render_component( component )} - %endfor -
    + Folder: ${this_folder.name} + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> +
    + %for component in components: + ${render_component( component )} + %endfor +
    - - - - - - <%def name="title()">View Library: ${library.name} - -
    +
    Import from Library: ${library.name}
    ${render_folder( library.root_folder )} -
    -
    - diff --git a/templates/library/manage_dataset.mako b/templates/library/manage_dataset.mako deleted file mode 100644 index f04615def51..00000000000 --- a/templates/library/manage_dataset.mako +++ /dev/null @@ -1,100 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Edit Dataset Attributes - - -<%def name="datatype( dataset, datatypes )"> - - - -
    -
    Edit Attributes
    -
    -
    - -
    - -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - %for element in metadata: -
    - -
    - ${element.get_html()} -
    -
    -
    - %endfor -
    - -
    -
    -
    - -
    - -
    -
    - This will inspect the dataset and attempt to correct the above column values - if they are not accurate. -
    -
    -
    -
    - -

    - - -

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

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

    -
    -
    ${data.display_name()}
    -
    -
    - - ## Header row for history items (name, state, action buttons) - -
    - %if data_state != 'ok': -
    - %endif -
    -
    - ##display data - edit attributes - ##delete -
    - ##${data.display_name()} -
    - - ## Body for history items, extra info and actions, data "peek" - -
    - %if data_state == "queued": -
    Job is waiting to run
    - %elif data_state == "running": -
    Job is currently running
    - %elif data_state == "error": -
    - An error occurred running this job: ${data.display_info().strip()}, - report this error -
    - %elif data_state == "empty": -
    No data: ${data.display_info()}
    - %elif data_state == "ok": -
    - ${data.blurb}, - format: ${data.ext}, - database: - %if data.dbkey == '?': - ${data.dbkey} - %else: - ${data.dbkey} - %endif -
    -
    Info: ${data.display_info()}
    - %if data.peek != "no peek": -
    ${data.display_peek()}
    - %endif - %else: -
    Error: unknown dataset state "${data_state}".
    - %endif - - ## Recurse for child datasets - - -
    -
    -
    - - -## Render a folder -<%def name="render_folder( this_folder )"> - -
    -
    Contents of Folder: ${this_folder.name}
    -
    -
    -
    - <% - components = list( this_folder.folders ) + list( this_folder.datasets ) - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> - %for component in components: - ${render_component( component )} - %endfor - -
    -
    -
    - -
    - -
    -
    - - - - - - -<%def name="title()">Manage Folder: ${folder.name} - -
    -
    Change Folder Attributes
    -
    -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - - - -
    - -
    -
    Manage Folder Contents: ${folder.name}
    -
    -
    - %if folder.parent: - up a level - %elif folder.library_root: - manage library - %endif -
    -
    -
    - ${render_folder( folder )} -
    - -
    - -
    - -
    -
    -
    - diff --git a/templates/library/manage_library.mako b/templates/library/manage_library.mako deleted file mode 100644 index 5d94dfb6b78..00000000000 --- a/templates/library/manage_library.mako +++ /dev/null @@ -1,46 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Manage Library - - -
    -
    Edit a Library: ${library.name}
    -
    -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - diff --git a/templates/library/new_dataset.mako b/templates/library/new_dataset.mako deleted file mode 100644 index 1c517cdfede..00000000000 --- a/templates/library/new_dataset.mako +++ /dev/null @@ -1,997 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Create New Library Dataset - -
    -
    Create a new Library Dataset
    -
    -
    - - - -
    - - -
    - - -
    - - -
    - -
    - - -
    - -
    - Here you may specify a list of URLs (one per line) or paste the contents of a file. -
    - - - -
    - -
    - -
    - - -
    Yes
    - -
    - - Use this option if you are entering intervals by hand. -
    - - -
    - -
    - -
    - - -
    - -
    - Which format? See help below -
    - - -
    - -
    - -
    - - -##this should be generated dynamically -
    - - - -
    - -
    - - -
    - -
    - - -
    -
    -
    - diff --git a/templates/library/user_list_libraries.mako b/templates/library/user_list_libraries.mako deleted file mode 100644 index 23a2b3b9ad1..00000000000 --- a/templates/library/user_list_libraries.mako +++ /dev/null @@ -1,14 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">View Libraries - -
    -
    View Library
    -
    - %for library in libraries: - - %endfor - -
    -
    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/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 From d392b887ec32dd4ba4e76fc2d8b57e780e8b5e43 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 14 Aug 2008 17:44:06 -0400 Subject: [PATCH 15/94] Fix a permitted_actions bug, and re-enable the ability for a user to make a single dataset private or public. And fix a few typos. --- lib/galaxy/security/__init__.py | 20 ++++++++++++++++---- lib/galaxy/web/controllers/async.py | 2 +- lib/galaxy/web/controllers/root.py | 17 +++++++---------- templates/dataset/edit_attributes.mako | 10 ++++------ templates/root/history_common.mako | 2 +- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index ed73ec2364e..7709c5a1916 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -81,9 +81,8 @@ class GalaxyRBACAgent( RBACAgent ): # 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 ): - for pa in group_dataset_assoc.permitted_actions: - if action in pa.permitted_actions.actions: - return True + 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. @@ -132,6 +131,12 @@ class GalaxyRBACAgent( RBACAgent ): 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=[] ): # TODO, Nate: Make sure this method is functionally correct. # TODO: For now, just take the dataset's permitted_actions, but we need to make sure @@ -145,6 +150,11 @@ class GalaxyRBACAgent( RBACAgent ): assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) assoc.flush() return assoc + def disassociate_group_dataset( self, group, dataset ): + log.debug("In disassociate_group_dataset, removing %s -> %s" % (group.id, dataset.id)) + 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() @@ -182,7 +192,7 @@ class GalaxyRBACAgent( RBACAgent ): # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. if groups is None: if history.user: - groups = history.user.default_groups + groups = [ assoc.group for assoc in history.user.default_groups ] else: groups = [ self.get_public_group() ] if groups is not None: @@ -234,6 +244,8 @@ class GalaxyRBACAgent( RBACAgent ): 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''' diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index 004868434d8..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) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 8841c46eedd..dc58f02f2ee 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -262,30 +262,27 @@ class RootController( BaseController ): if target_type: msg = data.datatype.convert_dataset(trans, data, target_type) return trans.show_ok_message( msg, refresh_frames=['history'] ) - ''' - # Users can't currently change permissions or groups - elif p.change_permision: - """The user clicked the change_permision button on the 'Change permissions' form""" + 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 data.dataset.has_group( 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.REMOVE_GROUP, dataset = data.dataset ): - trans.app.security_agent.remove_component_association( dataset = data, group = 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 data.dataset.has_group( public_group ): + 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.ADD_GROUP, dataset = data.dataset ): + 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 ) diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 1479b15c20a..c54f6eb0d1a 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -133,19 +133,18 @@

    -<%doc> -%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.REMOVE_GROUP, dataset = data ) or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.ADD_GROUP, dataset = data ) ): +%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ):

    Change Permitted Actions
    -
    +
    <% checked = "" %> - %if not data.dataset.has_group( trans.app.model.Group.get_public_group() ): + %if not trans.app.security_agent.dataset_has_group( data.id, trans.app.model.Group.get_public_group().id ): <% checked = " checked" %> %endif
    @@ -158,10 +157,9 @@
    - +
    %endif - diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index aa9a5c238a5..cd3225e7994 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -33,7 +33,7 @@
    %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): -
    You do not have permision to view this dataset.
    +
    You do not have permission to view this dataset.
    %elif data_state == "queued":
    Job is waiting to run
    %elif data_state == "running": From a22b2ba86d82f1c7f16f57506d9e8bbe6c2f7e34 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 14 Aug 2008 18:19:38 -0400 Subject: [PATCH 16/94] Default to only adding datasets to a user's private group, and ensure externally authenticated users get their defaults set up. --- lib/galaxy/security/__init__.py | 2 +- lib/galaxy/web/framework/__init__.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 7709c5a1916..81db90994b6 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -171,7 +171,7 @@ class GalaxyRBACAgent( RBACAgent ): 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.get_public_group(), self.create_private_user_group( user ) ] + 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() diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 124d1549453..87f099a681d 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -272,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 ): From 849a462b5c6c3631e3e3c12c09521a4cf50dd722 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 15 Aug 2008 09:07:02 -0400 Subject: [PATCH 17/94] Fix for sharing a history with another user, still need to apply security checks. --- lib/galaxy/model/__init__.py | 71 ++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 460b6433b1d..599ab72a78b 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -417,7 +417,12 @@ class DatasetInstance( object ): 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 ): + 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 @@ -426,10 +431,19 @@ class HistoryDatasetAssociation( DatasetInstance ): self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association def copy( self, copy_children = False, parent_id = None ): - print "self.dataset", self.dataset - - 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 ) - print "des data", des.dataset + 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: @@ -501,14 +515,13 @@ class History( object ): des.flush() des.name = self.name for data in self.datasets: - new_data = data.copy( copy_children = True, target_user = target_user ) + 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" @@ -531,7 +544,12 @@ class LibraryFolder( object ): 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 ): + 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 @@ -539,7 +557,18 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 = 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 ) @@ -549,7 +578,18 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 = 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: @@ -626,7 +666,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 From 199e3327df08b8ebf497936fdcd29c4fc07d01ce Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 15 Aug 2008 16:40:13 -0400 Subject: [PATCH 18/94] Feature enhancements and cleanup for dataset security and libraries. 1) DefaultHistoryGroupAssociations are now deleted when a history is deleted. 2) New user's private group name now includes 'private group' 3) When an admin adds a new dataset to a library folder, they can associate it with 1 or more groups and then edit the permitted_actions on the edit page for the dataset. 4) Other miscellaneous cleanup --- lib/galaxy/model/mapping.py | 2 - lib/galaxy/security/__init__.py | 23 +- lib/galaxy/web/controllers/admin.py | 128 +++- lib/galaxy/web/controllers/dataset.py | 5 +- lib/galaxy/web/controllers/root.py | 7 +- templates/admin/library/dataset.mako | 35 + templates/admin/library/folder.mako | 4 +- templates/admin/library/new_dataset.mako | 919 +---------------------- 8 files changed, 184 insertions(+), 939 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index e6f132693da..2303c9eb338 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -186,7 +186,6 @@ Library.table = Table( "library", metadata, 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 ), @@ -217,7 +216,6 @@ LibraryTagDatasetAssociation.table = Table( "library_tag_dataset_association", m 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 ), diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 81db90994b6..c6790b2d7e3 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -21,7 +21,7 @@ class RBACAgent: 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' + DATASET_ACCESSS = '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 ) @@ -67,9 +67,8 @@ class GalaxyRBACAgent( RBACAgent ): 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 ): - # TODO, Nate: Make sure this method is functionally correct. """Returns true when user has permission to perform an action""" - while not isinstance( dataset, self.model.Dataset ): + 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 @@ -122,7 +121,6 @@ class GalaxyRBACAgent( RBACAgent ): return rval raise 'No valid method of creating group with %s' % ( kwd ) def associate_components( self, **kwd ): - # TODO, Nate: Make sure this method is functionally correct. assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to associate.' if 'dataset' in kwd: if 'group' in kwd: @@ -138,15 +136,11 @@ class GalaxyRBACAgent( RBACAgent ): 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=[] ): - # TODO, Nate: Make sure this method is functionally correct. - # TODO: For now, just take the dataset's permitted_actions, but we need to make sure - # we can associate group permitted_actions if necessary - need to look into this... if not permitted_actions: if isinstance( dataset.permitted_actions, Bunch ): permitted_actions = dataset.permitted_actions.__dict__.values() else: permitted_actions = dataset.permitted_actions - log.debug("In associate_group_dataset, permitted_actions: %s" %str(permitted_actions) ) assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) assoc.flush() return assoc @@ -160,9 +154,9 @@ class GalaxyRBACAgent( RBACAgent ): assoc.flush() return assoc def create_private_user_group( self, user ): - # TODO, Nate: Make sure this method is functionally correct. # Create private group - group = self.model.Group( user.email, priority = 10 ) + 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 ) @@ -177,12 +171,10 @@ class GalaxyRBACAgent( RBACAgent ): assoc.delete() assoc.flush() for group in groups: - log.debug("In user_set_default_access, group: %s" %str(group)) if isinstance( group, self.model.Group ): permitted_actions = group.permitted_actions.__dict__.values() else: permitted_actions = group.permitted_actions - log.debug("In user_set_default_access, permitted_actions: %s" % str( permitted_actions)) assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) assoc.flush() if history: @@ -200,12 +192,10 @@ class GalaxyRBACAgent( RBACAgent ): assoc.delete() assoc.flush() for group in groups: - log.debug("In history_set_default_access, group: %s" %str(group)) if isinstance( group, self.model.Group ): permitted_actions = group.permitted_actions.__dict__.values() else: permitted_actions = group.permitted_actions - log.debug("In history_set_default_access, permitted_actions: %s" % str( permitted_actions)) assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) assoc.flush() if dataset: @@ -223,7 +213,6 @@ class GalaxyRBACAgent( RBACAgent ): def guess_public_group( self ): return self.model.Group.guess_public_group() def set_dataset_groups( self, dataset, groups ): - # TODO, Nate: Make sure this method is functionally correct. if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset for group_dataset_assoc in dataset.groups: @@ -232,7 +221,6 @@ class GalaxyRBACAgent( RBACAgent ): for group in groups: if not isinstance( group, self.model.Group ): group = group.group - log.debug("In set_dataset_groups, before elf.associate_components, dataset: %s, group: %s" % ( str(dataset), str(group))) self.associate_components( dataset=dataset, group=group ) def get_component_associations( self, **kwd ): # TODO, Nate: Make sure this method is functionally correct. @@ -254,6 +242,5 @@ def get_permitted_actions( self, filter=None ): 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)] + [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/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 3367ce6bc36..55f4f850646 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,5 +1,6 @@ -import shutil, StringIO +import shutil, StringIO, operator +from galaxy import util from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent @@ -540,7 +541,7 @@ class Admin( BaseController ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) data_files = [] - def add_file( file_obj, name, extension, dbkey, info = 'no info', space_to_tab = False ): + def add_file( file_obj, name, extension, dbkey, groups, info='no info', space_to_tab=False ): data_type = None temp_name = sniff.stream_to_file( file_obj ) if space_to_tab: @@ -555,8 +556,14 @@ class Admin( BaseController ): folder = trans.app.model.LibraryFolder.get( folder_id ) folder.add_dataset( dataset ) dataset.flush() - # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public - trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) + # 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() @@ -575,7 +582,7 @@ class Admin( BaseController ): return dataset if 'create_dataset' in kwd: - #copied from upload tool action + # Copied from upload tool action last_dataset_created = None data_file = kwd['file_data'] url_paste = kwd['url_paste'] @@ -583,6 +590,12 @@ class Admin( BaseController ): 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 = [] @@ -590,14 +603,26 @@ class Admin( BaseController ): 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, info="uploaded file", space_to_tab = space_to_tab ) + last_dataset_created = add_file( data_file.file, + file_name, + extension, + dbkey, + 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, info="uploaded url", space_to_tab=space_to_tab ) + last_dataset_created = add_file( urllib.urlopen( line ), + line, + extension, + dbkey, + groups, + info="uploaded url", + space_to_tab=space_to_tab ) else: is_valid = False for line in url_paste: @@ -606,16 +631,73 @@ class Admin( BaseController ): is_valid = True break if is_valid: - last_dataset_created = add_file( StringIO.StringIO( url_paste ), 'Pasted Entry', extension, dbkey, info="pasted entry", space_to_tab=space_to_tab ) - trans.response.send_redirect( web.url_for( action='dataset', id = last_dataset_created.id ) ) - #return self.dataset( trans, id = last_dataset_created.id ) + last_dataset_created = add_file( StringIO.StringIO( url_paste ), + 'Pasted Entry', + extension, + dbkey, + 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: - return trans.fill_template( '/admin/library/new_dataset.mako', folder_id = folder_id ) + # 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 = trans.history.genome_build + for dbkey, build_name in util.dbnames: + yield build_name, dbkey, ( dbkey==last_used_build ) + dbkeys = get_dbkey_options() + # 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, + groups=groups ) dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) if dataset: - #copied from edit attributes for 'regular' datasets + # Copied from edit attributes for 'regular' datasets with some additions p = util.Params(kwd, safe=False) - if p.change: + 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 ] + # 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() @@ -623,7 +705,6 @@ class Admin( BaseController ): # 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"): @@ -651,7 +732,20 @@ class Admin( BaseController ): 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 DatasetGroupAssociations + dataset.dataset.refresh() + for group_dataset_assoc in dataset.dataset.groups: + 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 @@ -670,7 +764,9 @@ class Admin( BaseController ): return trans.fill_template( "/admin/library/dataset.mako", dataset=dataset, metadata=metadata, - datatypes=ldatatypes, + 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/dataset.py b/lib/galaxy/web/controllers/dataset.py index d38537509cd..3d43b5bef5e 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -105,8 +105,7 @@ class DatasetInterface( BaseController ): """Catches the dataset id and displays file contents as directed""" data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) if not data: - raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) - # TODO, Nate: Make sure the following is functionally correct. + 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() ) @@ -127,4 +126,4 @@ class DatasetInterface( BaseController ): except: raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: - raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." ) + raise paste.httpexceptions.HTTPForbidden( "You are not permitted to access this dataset." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index dc58f02f2ee..6850b069cb7 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -395,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() diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index 52a35099045..b522a05256d 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -13,6 +13,41 @@ %endfor +
    +
    Group Associations
    +
    +
    + + %for gda in gdas: +
    + ${gda[1]} +
    +
    +
    + %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 +
    +
    + %endfor +
    +
    +
    +
    Edit Attributes
    diff --git a/templates/admin/library/folder.mako b/templates/admin/library/folder.mako index b4b11c74b36..e93b64e7989 100644 --- a/templates/admin/library/folder.mako +++ b/templates/admin/library/folder.mako @@ -94,9 +94,9 @@
    diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 241e121ebeb..35a8106fd81 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -37,24 +37,9 @@
    @@ -64,889 +49,29 @@
    - ##this should be generated dynamically
    -
    + +
    - + + Multi-select list - hold the appropriate key while clicking to select multiple columns +
    + +
    +
    +
    +
    +
    From 140d8b5f7d5aaadf3f0ffbceced65041b0652456 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 18 Aug 2008 09:00:53 -0400 Subject: [PATCH 19/94] Fix a typo in the security agent ( my last commit ) that broke a bunch of stuff. --- lib/galaxy/security/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index c6790b2d7e3..b36881505e0 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -21,7 +21,7 @@ class RBACAgent: 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_ACCESSS = 'dataset_access' + 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 ) @@ -68,6 +68,7 @@ class GalaxyRBACAgent( RBACAgent ): 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""" + log.debug("In allow_dataset_action, user: %s, action: %s, dataset: %s" % ( str(user), str(action), str(dataset))) if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset # If dataset is in public group, we always return true for viewing and using @@ -238,9 +239,11 @@ class GalaxyRBACAgent( RBACAgent ): def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' if filter is None: + log.debug("In get_permitted_actions, returning RBACAgent.permitted_actions: %s" % str( RBACAgent.permitted_actions)) 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)] + log.debug("In get_permitted_actions, returning tmp_bunch: %s" % str( tmp_bunch)) return tmp_bunch From 751cca164bb68cf24325f89be9f877ac4814527d Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 18 Aug 2008 11:21:31 -0400 Subject: [PATCH 20/94] LibraryFolders now track last used dbkey ( silimar to histories ), so when a new dataset is added to a folder, the last used dbkey is selected. --- lib/galaxy/model/__init__.py | 36 +++----------- lib/galaxy/model/mapping.py | 4 +- lib/galaxy/security/__init__.py | 4 -- lib/galaxy/web/controllers/admin.py | 60 ++++++++++++++++-------- templates/admin/library/new_dataset.mako | 6 ++- 5 files changed, 53 insertions(+), 57 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 599ab72a78b..4ef5a34744e 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -285,34 +285,26 @@ class DatasetInstance( object ): 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() @@ -321,11 +313,8 @@ class DatasetInstance( object ): # 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). - """ + # 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] @@ -343,7 +332,6 @@ class DatasetInstance( object ): #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 ) @@ -400,16 +388,12 @@ class DatasetInstance( object ): 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: @@ -429,7 +413,6 @@ class HistoryDatasetAssociation( DatasetInstance ): 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, @@ -451,7 +434,6 @@ class HistoryDatasetAssociation( DatasetInstance ): 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: @@ -469,7 +451,6 @@ class History( object ): self.user = user self.datasets = [] self.galaxy_sessions = [] - def _next_hid( self ): # TODO: override this with something in the database that ensures # better integrity @@ -481,13 +462,11 @@ class History( object ): 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 ) @@ -507,7 +486,6 @@ class History( object ): 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 @@ -534,10 +512,13 @@ class LibraryFolder( object ): self.description = description self.item_count = item_count self.order_id = order_id - def add_dataset( self, dataset ): + 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 @@ -555,7 +536,6 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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, @@ -575,8 +555,6 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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, @@ -597,11 +575,9 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 2303c9eb338..cd4aa9c3ee9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -66,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 ), @@ -194,7 +193,8 @@ LibraryFolder.table = Table( "library_folder", metadata, Column( "name", TEXT ), Column( "description", TEXT ), Column( "order_id", Integer ), - Column( "item_count", Integer ) ) + Column( "item_count", Integer ), + Column( "genome_build", TrimmedString( 40 ) ) ) LibraryTag.table = Table( "library_tag", metadata, Column( "id", Integer, primary_key=True ), diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index b36881505e0..0eaba298d10 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -68,7 +68,6 @@ class GalaxyRBACAgent( RBACAgent ): 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""" - log.debug("In allow_dataset_action, user: %s, action: %s, dataset: %s" % ( str(user), str(action), str(dataset))) if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset # If dataset is in public group, we always return true for viewing and using @@ -146,7 +145,6 @@ class GalaxyRBACAgent( RBACAgent ): assoc.flush() return assoc def disassociate_group_dataset( self, group, dataset ): - log.debug("In disassociate_group_dataset, removing %s -> %s" % (group.id, dataset.id)) assoc = self.model.GroupDatasetAssociation.selectone_by( group_id = group.id, dataset_id = dataset.id ) assoc.delete() assoc.flush() @@ -239,11 +237,9 @@ class GalaxyRBACAgent( RBACAgent ): def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' if filter is None: - log.debug("In get_permitted_actions, returning RBACAgent.permitted_actions: %s" % str( RBACAgent.permitted_actions)) 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)] - log.debug("In get_permitted_actions, returning tmp_bunch: %s" % str( tmp_bunch)) return tmp_bunch diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 55f4f850646..cb8aad42c78 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -513,6 +513,10 @@ class Admin( BaseController ): 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 ) @@ -540,8 +544,17 @@ class Admin( BaseController ): 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 = [] - def add_file( file_obj, name, extension, dbkey, groups, info='no info', space_to_tab=False ): + + # 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: @@ -552,9 +565,13 @@ class Admin( BaseController ): 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 ) + 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 ) + 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 @@ -575,12 +592,12 @@ class Admin( BaseController ): 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 @@ -603,12 +620,13 @@ class Admin( BaseController ): 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_dataset_created = add_file( data_file.file, + file_name, + extension, + dbkey, + last_used_build, groups, - info="uploaded file", + 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: @@ -618,10 +636,11 @@ class Admin( BaseController ): if line: last_dataset_created = add_file( urllib.urlopen( line ), line, - extension, - dbkey, + extension, + dbkey, + last_used_build, groups, - info="uploaded url", + info="uploaded url", space_to_tab=space_to_tab ) else: is_valid = False @@ -632,22 +651,22 @@ class Admin( BaseController ): break if is_valid: last_dataset_created = add_file( StringIO.StringIO( url_paste ), - 'Pasted Entry', - extension, - dbkey, + 'Pasted Entry', + extension, + dbkey, + last_used_build, groups, - info="pasted entry", + 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 = trans.history.genome_build + 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() + 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' ), @@ -660,6 +679,7 @@ class Admin( BaseController ): 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: diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 35a8106fd81..3c641288b74 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -52,7 +52,11 @@
    From f50ee0fff3fcf803778f3a38f76852ac2c987365 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 19 Aug 2008 10:04:30 -0400 Subject: [PATCH 21/94] Added security to libraries. Non-admin users can now only see libraries that contain datasets associated with the user's groups, and each library will only display those datasets that are associated with the user's groups. --- lib/galaxy/web/controllers/admin.py | 6 +- lib/galaxy/web/controllers/library.py | 81 ++++++++++++++++++++++++-- templates/admin/library/dataset.mako | 45 +++++++------- templates/library/libraries.mako | 6 +- templates/library/library.mako | 31 +++++++++- tools/data_source/access_libraries.xml | 14 ++--- 6 files changed, 142 insertions(+), 41 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index cb8aad42c78..f0ccc2f4f07 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -703,6 +703,8 @@ class Admin( BaseController ): 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: @@ -761,9 +763,11 @@ class Admin( BaseController ): 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 DatasetGroupAssociations + # 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: diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 94e7b5731e0..43c45a58402 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -6,9 +6,12 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose - def index( self, trans, library_id = None, import_ids = [], **kwd ): - #use for importing an entry into your history + 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() @@ -19,5 +22,75 @@ class Library( BaseController ): history.flush() return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) elif library_id: - return trans.fill_template( '/library/library.mako', library=trans.app.model.Library.get( library_id ) ) - return trans.fill_template( '/library/libraries.mako', libraries=trans.app.model.Library.select() ) + # 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/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index b522a05256d..a7877e54479 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -1,6 +1,7 @@ <%inherit file="/base.mako"/> <%def name="title()">Edit Dataset Attributes + <%def name="datatype( dataset, datatypes )"> + +<%def name="group_dataset_permitted_actions( dataset_actions, gda )"> + %for da in dataset_actions: + <% check = False %> + %for action in gda[2]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
    + %endfor + +
    Group Associations
    %for gda in gdas: -
    - ${gda[1]} -
    +
    ${gda[1]}
    - %for da in dataset_actions: - <% check = False %> - %for action in gda[2]: - %if action == da: - <% - check = True - break - %> - %endif - %endfor - %if check: - - %else: - - %endif - ${da}
    - %endfor -
    + ${group_dataset_permitted_actions( dataset_actions, gda )}
    %endfor
    diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako index 247333f709c..1176bd2162a 100644 --- a/templates/library/libraries.mako +++ b/templates/library/libraries.mako @@ -1,12 +1,12 @@ <%inherit file="/base.mako"/> -<%def name="title()">View Libraries +<%def name="title()">Libraries You Can Access
    -
    View Library
    +
    Libraries You Can Access
    %for library in libraries: %endfor
    diff --git a/templates/library/library.mako b/templates/library/library.mako index 2706efcde25..373ff9c4118 100644 --- a/templates/library/library.mako +++ b/templates/library/library.mako @@ -3,17 +3,41 @@ <%def name="render_component( component )"> <% if isinstance( component, trans.app.model.LibraryFolder ): - return render_folder( component ) + 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 ): - return render_dataset( component ) + render = False + dataset = trans.app.model.Dataset.get( component.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + if render: + return render_dataset( component ) %> + ## Render the dataset `data` as history item, using `hid` as the displayed id <%def name="render_dataset( data )">
    ${data.name}
    + ## Render a folder <%def name="render_folder( this_folder )">
    @@ -31,11 +55,12 @@
    + <%def name="title()">View Library: ${library.name}
    Import from Library: ${library.name}
    - + ${render_folder( library.root_folder )}
    diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml index ffd383c481a..e6dabfbf1b1 100644 --- a/tools/data_source/access_libraries.xml +++ b/tools/data_source/access_libraries.xml @@ -1,11 +1,7 @@ - - stored locally - - - - - - - + stored locally + + + + \ No newline at end of file From 64b785cdc7e1ff1e7022d2ca92d6dd5f10643075 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 19 Aug 2008 16:46:38 -0400 Subject: [PATCH 22/94] Enhancement to the Admin GUI: added link to Libraries that a specific user can access because they contain datasets associated with 1 of the user's groups. Also optimized some code in the library controller. --- lib/galaxy/web/controllers/admin.py | 56 ++++++++++- lib/galaxy/web/controllers/library.py | 96 ++++++++----------- .../specified_users_groups.mako | 22 ++++- .../specified_users_group_libraries.mako | 27 ++++++ templates/library/library.mako | 1 - 5 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 templates/admin/library/specified_users_group_libraries.mako diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f0ccc2f4f07..f3ff2773608 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -461,6 +461,7 @@ class Admin( BaseController ): galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) for row2 in q2.execute(): total_datasets = row2.total_datasets + libraries = [] permitted_actions = [] # There may not yet be any GroupDatasetAssociations, in which case no # actions will be found @@ -468,16 +469,42 @@ class Admin( BaseController ): for action in row2.permitted_actions: permitted_actions.append( action.encode( 'ascii' ) ) permitted_actions.sort() + # If we have permitted actions, then we have at least 1 GroupDatasetAssociation, in + # which case, we can see if we have any Libraries that the user can access + libs = trans.app.model.Library.select() + for library in libs: + folder = library.root_folder + components = list( folder.folders ) + list( folder.datasets ) + for component in components: + if self.renderable( trans, component, row2.group_id ): + libraries.append( library.id ) + break groups.append( ( row.group_id, escape( row.group_name, entities ), row.group_priority, row2.total_datasets, - permitted_actions ) ) + permitted_actions, + libraries ) ) return trans.fill_template( '/admin/dataset_security/specified_users_groups.mako', user_id=user_id, user_email=escape( user_email, entities ), - groups=groups, + groups=groups, msg=msg ) + @web.expose + def specified_users_group_libraries( 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 + library_ids = params.library_ids.split( ',' ) + libraries = [] + for id in library_ids: + library = trans.app.model.Library.get( id ) + libraries.append( library ) + return trans.fill_template( '/admin/library/specified_users_group_libraries.mako', + user_email=params.user_email, + group_name=params.group_name, + libraries=libraries ) # Galaxy Library Stuff @web.expose @@ -794,3 +821,28 @@ class Admin( BaseController ): err=None ) else: return trans.show_error_message( "Invalid dataset specified" ) + def renderable( self, trans, component, group_id ): + render = False + if isinstance( component, trans.app.model.LibraryFolder ): + # 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 == group_id: + render = True + break + # Check the folder's sub-folders to see what can be rendered + if not render: + for library_folder in component.folders: + self.renderable( trans, library_folder, group_id ) + 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 == group_id: + render = True + break + return render + diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 43c45a58402..5514fe113eb 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -33,64 +33,46 @@ class Library( BaseController ): 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 ) + else: + 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 ) + 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: - 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: + folder = library.root_folder + components = list( folder.folders ) + list( folder.datasets ) + for component in components: + if self.renderable( trans, component, group_ids ): + libraries.append( 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 ) - 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 ) + def renderable( self, trans, component, group_ids ): + render = False + if isinstance( component, trans.app.model.LibraryFolder ): + # Check the folder's datasets to see what can be rendered + for library_folder_dataset_assoc in component.datasets: + if render: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + # Check the folder's sub-folders to see what can be rendered + if not render: + for library_folder in component.folders: + self.renderable( trans, library_folder, group_ids ) + 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 + return render diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako index 8a392a67e31..e5678635c96 100644 --- a/templates/admin/dataset_security/specified_users_groups.mako +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -17,20 +17,27 @@

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

    %if msg: - + %endif %if len( groups ) == 0: - + %else: + <% ctr = 0 %> %for group in groups: - <% gn = unescape( group[1], unentities ) %> + <% + gn = unescape( group[1], unentities ) + library_ids = '' + for library_id in group[5]: + library_ids += "%s," % library_id + library_ids = library_ids.rstrip( ',' ) + %> %if ctr % 2 == 1: %else: @@ -48,9 +55,16 @@ ${da}
    %endfor + <% ctr += 1 %> - %endfor + %endfor %endif

    ${msg}

    ${msg}

    User '${email}' belongs to no groups
    User '${email}' belongs to no groups
    Group Priority Datasets Permitted Actions on DatasetsContaining Libraries
    + %if len( group[5] ) > 0: + ${len( group[5] )} + %else: + ${len( group[5] )} + %endif +
    diff --git a/templates/admin/library/specified_users_group_libraries.mako b/templates/admin/library/specified_users_group_libraries.mako new file mode 100644 index 00000000000..37241869cf3 --- /dev/null +++ b/templates/admin/library/specified_users_group_libraries.mako @@ -0,0 +1,27 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Specified Users Group Libraries{ +<% + gn = unescape( group_name, unentities ) + email = unescape( user_email, unentities ) +%> +
    +
    + Libraries  |   + Groups  |   + Users +
    +
    Libraries containing datasets associated with group '${gn}' that user '${email}' can access
    +
    + %for library in libraries: + + %endfor +
    +
    diff --git a/templates/library/library.mako b/templates/library/library.mako index 373ff9c4118..3c3002caa5d 100644 --- a/templates/library/library.mako +++ b/templates/library/library.mako @@ -13,7 +13,6 @@ 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 ) From 3802816b69b2bca3341e3386b3dd38d47986fed7 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 20 Aug 2008 18:11:04 -0400 Subject: [PATCH 23/94] User interface to permissions, and a bunch of changes to ensure that both permitted_actions and groups are set appropriately on datasets. Somewhat tested and mostly working, but needs more testing. Greg: If a userless session has a history, if that user logs in, the DefaultHistoryGroupAssociation for the current history as well as the datasets in that history are supposed to be updated with the permissions in DefaultUserGroupAssociation, but this isn't happening. Possibly because the default permissions on userless histories create datasets in the public group with only the DATASET_ACCESS permission (or possibly for another reason). What are the implications of giving public users DATASET_MANAGE_PERMISSIONS? --- lib/galaxy/model/mapping.py | 2 +- lib/galaxy/security/__init__.py | 165 ++++++++++++--------- lib/galaxy/tools/__init__.py | 6 +- lib/galaxy/tools/actions/__init__.py | 7 +- lib/galaxy/tools/actions/upload.py | 6 +- lib/galaxy/tools/parameters/basic.py | 10 +- lib/galaxy/web/controllers/async.py | 3 +- lib/galaxy/web/controllers/dataset.py | 2 +- lib/galaxy/web/controllers/root.py | 69 ++++----- lib/galaxy/web/controllers/user.py | 36 ++--- lib/galaxy/web/framework/__init__.py | 3 +- static/june_2007_style/blue/base.css | 6 +- templates/dataset/edit_attributes.mako | 85 ++++++++--- templates/history/permissions.mako | 56 +++++-- templates/user/permissions.mako | 55 +++++-- tools/data_source/encode_import_code.py | 3 +- tools/data_source/microbial_import_code.py | 3 +- tools/maf/maf_to_bed_code.py | 3 +- 18 files changed, 299 insertions(+), 221 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cd4aa9c3ee9..4922b4fd7d5 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -619,7 +619,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): orphans = result.Dataset.get_by( history_id = None ) if orphans: for dataset in orphans: - result.security_agent.set_dataset_groups( dataset, [ public_group ] ) + result.security_agent.set_dataset_permissions( dataset, [ ( public_group, result.security_agent.permitted_actions.DATASET_ACCESS ) ] ) else: result.security_agent.guess_public_group() log.debug( "Public Group identified as id = %s." % ( Group.public_id ) ) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 0eaba298d10..9c497437120 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -7,10 +7,6 @@ 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( @@ -23,9 +19,14 @@ class RBACAgent: # use in a job, etc). DATASET_ACCESS = 'dataset_access' ) + permitted_action_descriptions = Bunch( + DATASET_EDIT_METADATA = "Edit this dataset's metadata in the library", + DATASET_MANAGE_PERMISSIONS = "Manage the groups associated with this dataset (and those groups' permissions on the dataset)", + DATASET_ACCESS = "View, import, and perform analyses on this dataset" + ) 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 = [] ): + def guess_derived_permissions_for_datasets( self, datasets = [] ): raise "Unimplemented Method" def associate_components( self, **kwd ): raise 'No valid method of associating provided components: %s' % kwd @@ -35,12 +36,12 @@ class RBACAgent: 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 ): + def user_set_default_access( self, user, permissions = None, history = False, dataset = False ): raise "Unimplemented Method" def setup_new_user( self, user ): self.user_set_default_access( user, history = True, dataset = True ) self.associate_components( user=user, group=self.get_public_group() ) - def history_set_default_access( self, history, groups=None, dataset=False ): + def history_set_default_access( self, history, permissions=None, dataset=False ): raise "Unimplemented Method" def set_public_group( self, group ): raise "Unimplemented Method" @@ -48,7 +49,7 @@ class RBACAgent: raise "Unimplemented Method" def guess_public_group( self ): raise "Unimplemented Method" - def set_dataset_groups( self, dataset, groups ): + def set_dataset_permissions( self, dataset, permissions ): raise "Unimplemented Method" def set_dataset_permitted_actions( self, dataset ): raise "Unimplemented Method" @@ -56,6 +57,24 @@ class RBACAgent: raise "Unimplemented Method" def components_are_associated( self, **kwd ): return bool( self.get_component_associations( **kwd ) ) + def convert_permitted_action_strings( self, permitted_action_strings ): + """ + When getting permitted actions from an untrusted source like a + form, ensure that they match our actual permitted actions. + """ + return filter( lambda x: x is not None, [ self.permitted_actions.get( action_string ) for action_string in permitted_action_strings ] ) + def get_permitted_action_description( self, permitted_action ): + """ + Return the description of a permitted_action, regardless of + whether permitted_action is a key or value. + """ + if self.permitted_action_descriptions.get( permitted_action ) is not None: + return self.permitted_action_descriptions.get( permitted_action ) + else: + for k, v in self.permitted_actions.items(): + if v == permitted_action: + return self.permitted_action_descriptions.get( k ) + return permitted_action # so at least something useful is printable class GalaxyRBACAgent( RBACAgent ): def __init__( self, model, permitted_actions=None ): @@ -83,35 +102,41 @@ class GalaxyRBACAgent( RBACAgent ): 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 + def guess_derived_permissions_for_datasets( self, datasets=[] ): + """Returns a list of group/action tuples for the output dataset based upon provided datasets""" + intersect = None + priority_access_perms = None for dataset in datasets: # Determine access groups for output datasets - these groups are the # intersection across all inputs. If we end up with no intersection # between inputs, then we rely on priorities if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset - 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 ) + assocs = [ assoc for assoc in dataset.groups ] + for assoc in assocs: + if priority_access_perms is None or priority_access_perms[0].priority < assoc.group.priority: + priority_access_perms = ( assoc.group, assoc.permitted_actions ) + if intersect is None: + intersect = [ (a.group, a.permitted_actions) for a in assocs ] else: - access_groups.intersection_update( set( groups ) ) - # Complete lists for output dataset access - if access_groups: - access_groups = list( access_groups) - else: - access_groups = [] + # Intersect existing perms with new perms + #access_assocs = filter( lambda x: x.group in [ a.group for a in access_assocs ], assocs ) + new_intersect = [] + for group, actions in intersect: + matches = filter( lambda x: x.group == group, assocs ) + # Could a dataset ever have more than one GDA with the same group id? + if len( matches ) > 1: + log.error( "Unable to derive permissions, duplicate group_dataset_association rows exist for group %d, dataset %d" % (group.id, dataset.id) ) + elif len( matches ) == 1: + # compare permitted_actions + pa_intersect = filter( lambda x: x in actions, matches[0].permitted_actions ) + new_intersect.append( ( group, pa_intersect ) ) + intersect = new_intersect # If we have no groups left after intersection, take the highest priority group - if not access_groups: - if priority_access_group: - access_groups = [ priority_access_group ] - return access_groups + if not intersect: + if priority_access_perms: + intersect = [ priority_access_perms ] + return intersect def get_group( self, id ): return self.model.Group.get( id ) raise 'No valid method of retrieving requested group %s' % ( id ) @@ -125,29 +150,18 @@ class GalaxyRBACAgent( RBACAgent ): if 'dataset' in kwd: if 'group' in kwd: return self.associate_group_dataset( kwd['group'], kwd['dataset'] ) + elif 'permissions' in kwd: + return self.associate_group_dataset( kwd['permissions'][0], kwd['dataset'], kwd['permissions'][1] ) elif 'user' in kwd: if 'group' in kwd: return self.associate_user_group( kwd['user'], kwd['group'] ) raise 'No valid method of associating provided components: %s' % kwd - def 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 + def associate_group_dataset( self, group, dataset, permitted_actions=[ RBACAgent.permitted_actions.DATASET_ACCESS ] ): + # HACK: The default permitted_actions should really not be used... need to find cases where this is done and correct it + log.debug("In associate_permissions_dataset, dataset: %d, group: %d, permitted_actions: %s" % ( dataset.id, group.id, 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() @@ -161,66 +175,69 @@ class GalaxyRBACAgent( RBACAgent ): 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: + def user_set_default_access( self, user, permissions = None, history = False, dataset = False ): + if permissions is None: + permissions = [ ( self.create_private_user_group( user ), self.permitted_actions.__dict__.values() ) ] + if permissions is not None: for assoc in user.default_groups: #this is the association not the actual group assoc.delete() assoc.flush() - for group in groups: - if isinstance( group, self.model.Group ): - permitted_actions = group.permitted_actions.__dict__.values() - else: - permitted_actions = group.permitted_actions + for group, permitted_actions in permissions: + log.debug("In user_set_default_access, user: %s, group: %s, permitted_actions: %s" % (user.email, group.name, str( 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: + self.history_set_default_access( history, permissions=permissions, dataset=dataset ) + def user_get_default_access( self, user ): + return [ ( duga.group, duga.permitted_actions ) for duga in user.default_groups ] + def history_set_default_access( self, history, permissions=None, dataset=False ): + if permissions is None: if history.user: - groups = [ assoc.group for assoc in history.user.default_groups ] + permissions = self.user_get_default_access( history.user ) else: - groups = [ self.get_public_group() ] - if groups is not None: + # FIXME: should this be all permissions? + permissions = [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] + if permissions is not None: for assoc in history.default_groups: #this is the association not the actual group assoc.delete() assoc.flush() - for group in groups: - if isinstance( group, self.model.Group ): - permitted_actions = group.permitted_actions.__dict__.values() - else: - permitted_actions = group.permitted_actions + for group, permitted_actions in permissions: + log.debug("In history_set_default_access, history: %s, group: %s, permitted_actions: %s" % (history.id, group.name, str( 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() ] ) + self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) break else: - self.set_dataset_groups( data.dataset, groups ) + self.set_dataset_permissions( data.dataset, permissions ) + def history_get_default_access( self, history ): + return [ ( dhga.group, dhga.permitted_actions ) for dhga in history.default_groups ] def get_public_group( self ): return self.model.Group.get_public_group() def set_public_group( self, group ): return self.model.Group.set_public_group( group ) def guess_public_group( self ): return self.model.Group.guess_public_group() - def set_dataset_groups( self, dataset, groups ): + def set_dataset_permissions( self, dataset, permissions ): + """ + Apply permissions (a list of (group, permitted_action) tuples) + to a dataset, removing any existing permissions. permissions can + also be a list of GroupDatasetAssociations (for simplicity). + """ if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset for group_dataset_assoc in dataset.groups: group_dataset_assoc.delete() group_dataset_assoc.flush() - for group in groups: - if not isinstance( group, self.model.Group ): - group = group.group - self.associate_components( dataset=dataset, group=group ) + if isinstance( permissions[0], self.model.GroupDatasetAssociation ): + permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] + for ptuple in permissions: + log.debug("In set_dataset_permissions, before elf.associate_components, dataset: %s, group: %s, permitted_actions: %s" % ( str(dataset.id), str(ptuple[0].id), str(ptuple[1]))) + self.associate_components( dataset=dataset, permissions=ptuple ) def get_component_associations( self, **kwd ): # TODO, Nate: Make sure this method is functionally correct. assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 7b5c4b0d305..4030cadd47e 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,8 +1085,7 @@ class Tool: else: visible = False ext = fields.pop(0).lower() child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) - # TODO, Nate: Make sure the following is functionally correct. - self.app.security_agent.set_dataset_groups( child_dataset.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_permissions( child_dataset.dataset, outdata.dataset.groups ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1123,8 +1122,7 @@ class Tool: ext = fields.pop(0).lower() # Create new primary dataset primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) - # TODO, Nate: Make sure the following is functionally correct. - self.app.security_agent.set_dataset_groups( primary_data.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_permissions( primary_data.dataset, outdata.dataset.groups ) primary_data.flush() # Move data from temp location to dataset location shutil.move( filename, primary_data.file_name ) diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index bd74d188ee6..efc0ce99893 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -86,11 +86,10 @@ class DefaultToolAction( object ): # 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 ) + output_permissions = trans.app.security_agent.guess_derived_permissions_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 ] - + output_permissions = trans.app.security_agent.history_get_default_access( trans.history ) # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -132,7 +131,7 @@ class DefaultToolAction( object ): data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) # Commit the dataset immediately so it gets database assigned unique id data.flush() - trans.app.security_agent.set_dataset_groups( data.dataset, output_access_groups ) + trans.app.security_agent.set_dataset_permissions( data.dataset, output_permissions ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index a9623ae40e0..9fba4fe467d 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -66,8 +66,7 @@ class UploadToolAction( object ): def upload_empty(self, trans, err_code, err_msg): 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 ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( trans.history ) ) data.name = err_code data.extension = "txt" data.dbkey = "?" @@ -161,8 +160,7 @@ 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 ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( trans.history ) ) data.name = file_name data.dbkey = dbkey data.info = info diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index b626e665472..4167629927b 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -992,15 +992,15 @@ class DataToolParameter( ToolParameter ): >>> group.flush() >>> Group.public_id = group.id >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset1, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset1, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset2, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset2, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset3, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset3, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset4, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset4, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset5, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset5, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> hist.add_dataset( dataset1 ) >>> hist.add_dataset( dataset2 ) >>> hist.add_dataset( dataset3 ) diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index dcb6a0c1be5..f0e4ec01e7f 100644 --- a/lib/galaxy/web/controllers/async.py +++ b/lib/galaxy/web/controllers/async.py @@ -104,8 +104,7 @@ 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 ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( trans.history ) ) 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 3d43b5bef5e..8e7a09bfc9d 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -126,4 +126,4 @@ class DatasetInterface( BaseController ): except: raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: - raise paste.httpexceptions.HTTPForbidden( "You are not permitted to access this dataset." ) + return trans.show_error_message( "You are not privileged to access this dataset." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 6850b069cb7..83ab538e87f 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -14,8 +14,6 @@ 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): @@ -266,23 +264,18 @@ class RootController( BaseController ): """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." ) + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): + group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] + group_ids_checked = filter( lambda x: not x.count('_'), group_args ) + permissions = [] + for group_id in group_ids_checked: + action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] + actions = trans.app.security_agent.convert_permitted_action_strings( action_strings ) + permissions.append( ( trans.app.security_agent.get_group( group_id ), actions ) ) + trans.app.security_agent.set_dataset_permissions( data.dataset, permissions ) + return trans.show_ok_message( "Dataset permissions have been set.", refresh_frames=['history'] ) 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'] ) + return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) data.datatype.before_edit( data ) @@ -596,12 +589,12 @@ class RootController( BaseController ): """Adds a POSTed file to a History""" try: history = trans.app.model.History.get( history_id ) - groups = history.default_groups + groups = trans.app.security_agent.history_get_default_access( history ) if copy_access_from: copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) - groups = copy_access_from.dataset.groups + group_dataset_associations = 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 ) + trans.app.security_agent.set_dataset_permissions( data.dataset, group_dataset_associations ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -629,34 +622,24 @@ class RootController( BaseController ): 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: + group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] + group_ids_checked = filter( lambda x: not x.count('_'), group_args ) + if not group_ids_checked: 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." ) + permissions = [] + for group_id in group_ids_checked: + group = trans.app.security_agent.get_group( group_id ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] + permissions.append( ( group, trans.app.security_agent.convert_permitted_action_strings( action_strings ) ) ) + trans.app.security_agent.history_set_default_access( history, permissions = permissions ) + return trans.show_ok_message( 'Default history permitted actions have been changed.' ) 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""" diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index d46abb922c5..5b00894d1f9 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -170,33 +170,23 @@ class User( BaseController ): @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." ) + group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] + group_ids_checked = filter( lambda x: not x.count('_'), group_args ) + if not group_ids_checked: + return trans.show_error_message( "You must specify at least one default group." ) + permissions = [] + for group_id in group_ids_checked: + group = trans.app.security_agent.get_group( group_id ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] + permissions.append( ( group, trans.app.security_agent.convert_permitted_action_strings( action_strings ) ) ) + trans.app.security_agent.user_set_default_access( trans.user, permissions ) + return trans.show_ok_message( 'Default new history permitted actions have been changed.' ) return trans.fill_template( 'user/permissions.mako' ) else: # User not logged in, history group must be only public diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 87f099a681d..bd3741a939d 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -433,10 +433,9 @@ 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 ) + self.app.security_agent.history_set_default_access( history, dataset=True ) history.user_id = user.id history.flush() self.__history = history diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index e0ac97ea56b..2cf80e80722 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -414,4 +414,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index c54f6eb0d1a..712b2eccfc5 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -133,33 +133,80 @@

    -%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 ) ): +%if trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ): +

    -
    Change Permitted Actions
    +
    Change Dataset Access Permissions
    - - <% 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. -
    -
    -
    -
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% dataset_group_ids = [ assoc.group.id for assoc in data.dataset.groups ] %> +
    + Check each group which should have access to this dataset. +
    + %for group in user_groups: + %if group.id in dataset_group_ids: + <% assoc = filter( lambda x: x.group_id == group.id, data.dataset.groups )[0] %> + %else: + <% assoc = None %> + %endif + ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor
    +%elif trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ) ): +
    +
    Dataset Access Permissions
    +
    +
    + +
    + %for assoc in data.dataset.groups: + ${assoc.group.name} +
      + %for action in assoc.permitted_actions: +
    • ${trans.app.security_agent.get_permitted_action_description(action)}
    • + %endfor +
    + %endfor +
    + You do not have permission to edit this dataset's permissions. +
    +
    +
    +
    %endif diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako index 6607f002c41..013481d9351 100644 --- a/templates/history/permissions.mako +++ b/templates/history/permissions.mako @@ -2,6 +2,23 @@ <%def name="title()">Change Default History Permitted Actions %if trans.user: +
    Change Default History Permitted Actions
    @@ -9,27 +26,34 @@
    <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> <% cur_groups = [ assoc.group for assoc in trans.get_history().default_groups ] %> -
    - - %for group in user_groups: - + + checked + %else: + <% assoc = None %> + %endif + /> ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    %endfor -
    GroupInOut
    ${group.name}
    -
    - This will change the default permitted actions assigned to new datasets for your current history. + This will change the default permitted actions assigned + to new datasets in your current history. You may also + specify the defaults for new histories via the + user options. +
    @@ -39,4 +63,4 @@
    -%endif \ No newline at end of file +%endif diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako index 7b6b90f976d..e451bc71c29 100644 --- a/templates/user/permissions.mako +++ b/templates/user/permissions.mako @@ -2,6 +2,23 @@ <%def name="title()">Change Default History Permitted Actions %if trans.user: +
    Change Default Permitted Actions for new Histories
    @@ -9,27 +26,33 @@
    <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> <% cur_groups = [ assoc.group for assoc in trans.user.default_groups ] %> -
    - - %for group in user_groups: - + + checked + %else: + <% assoc = None %> + %endif + /> ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    %endfor -
    GroupInOut
    ${group.name}
    -
    - This will change the default permitted actions assigned to new datasets for new histories. + This will change the default permitted actions assigned + to new datasets in new histories. You may also specify + per-history defaults via the + history options.
    @@ -39,4 +62,4 @@
    -%endif \ No newline at end of file +%endif diff --git a/tools/data_source/encode_import_code.py b/tools/data_source/encode_import_code.py index ddfbb0241e0..19fb17fef95 100644 --- a/tools/data_source/encode_import_code.py +++ b/tools/data_source/encode_import_code.py @@ -38,8 +38,7 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + description + ")" history.add_dataset( newdata ) - #TODO, Nate: Make sure the following is functionally correct - app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) + app.security_agent.set_dataset_permissions( newdata.dataset, base_dataset.dataset.groups ) app.model.flush() try: copyfile(filepath,newdata.file_name) diff --git a/tools/data_source/microbial_import_code.py b/tools/data_source/microbial_import_code.py index e8816f093ff..b5ccbf11c3c 100644 --- a/tools/data_source/microbial_import_code.py +++ b/tools/data_source/microbial_import_code.py @@ -129,8 +129,7 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + microbe_info[kingdom][org]['chrs'][chr]['data'][description]['feature'] +" for "+microbe_info[kingdom][org]['name']+":"+chr + ")" newdata.flush() - #TODO, Nate: Make sure the following is functionally correct - app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) + app.security_agent.set_dataset_permissions( newdata.dataset, base_dataset.dataset.groups ) history.add_dataset( newdata ) app.model.flush() try: diff --git a/tools/maf/maf_to_bed_code.py b/tools/maf/maf_to_bed_code.py index 428486b3e37..d28c10b73ca 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -32,8 +32,7 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr 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 ) + app.security_agent.set_dataset_permissions( newdata.dataset, output_data.dataset.groups ) newdata.flush() history.flush() app.model.flush() From 9387c752445dcadbe68c8996fd7a694dab4fd2e3 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 21 Aug 2008 09:17:09 -0400 Subject: [PATCH 24/94] Added unique indexes to Library.name and Group.name, fixed some bugs in the admin controller. --- lib/galaxy/model/mapping.py | 4 +- lib/galaxy/web/controllers/admin.py | 37 +++++++++++++++---- .../admin/dataset_security/group_create.mako | 2 +- .../group_dataset_permitted_actions_edit.mako | 2 +- .../dataset_security/group_members_edit.mako | 2 +- templates/admin/library/libraries.mako | 3 ++ 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 4922b4fd7d5..d66c3ce391c 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -119,7 +119,7 @@ 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( "name", TEXT, index=True, unique=True ), Column( "priority", Integer ), Column( "deleted", Boolean, index=True, default=False ) ) @@ -182,7 +182,7 @@ Library.table = Table( "library", metadata, 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( "name", TEXT, index=True, unique=True ), Column( "description", TEXT ) ) LibraryFolder.table = Table( "library_folder", metadata, diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f3ff2773608..02bd8183e2f 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -134,6 +134,9 @@ class Admin( BaseController ): if not name: msg = "Please enter a name" trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) + elif len( trans.app.model.Group.select_by( name=name ) ) > 0: + msg = "A group with that name already exists" + trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) else: try: priority = int( params.priority ) @@ -144,6 +147,12 @@ class Admin( BaseController ): group.flush() # Add the members 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 = [] for user_id in members: user = galaxy.model.User.get( user_id ) # Create the UserGroupAssociation @@ -504,25 +513,33 @@ class Admin( BaseController ): return trans.fill_template( '/admin/library/specified_users_group_libraries.mako', user_email=params.user_email, group_name=params.group_name, - libraries=libraries ) + libraries=libraries, + 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() ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/library/libraries.mako', libraries=trans.app.model.Library.select(), msg=msg ) @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 ) + params = util.Params( kwd ) + msg = params.msg if 'create_library' in kwd: + if len( trans.app.model.Library.select_by( name=name ) ) > 0: + msg = "A library with that name already exists" + trans.response.send_redirect( web.url_for( action='libraries', msg=msg ) ) 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 ) ) + trans.response.send_redirect( web.url_for( action='folder', id=root_folder.id, msg=msg ) ) 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" ) @@ -531,13 +548,15 @@ class Admin( BaseController ): .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 ) + return trans.fill_template( '/admin/library/library.mako', library=library, msg=msg ) 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 ) + params = util.Params( kwd ) + msg = params.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 @@ -548,7 +567,7 @@ class Admin( BaseController ): 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 ) ) + trans.response.send_redirect( web.url_for( action='folder', id=folder.id, msg=msg ) ) 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" ) @@ -579,6 +598,8 @@ class Admin( BaseController ): folder = trans.app.model.LibraryFolder.get( folder_id ) last_used_build = folder.genome_build data_files = [] + params = util.Params( kwd ) + msg = params.msg # add_file method def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): @@ -707,7 +728,8 @@ class Admin( BaseController ): file_formats=file_formats, dbkeys=dbkeys, last_used_build=last_used_build, - groups=groups ) + groups=groups, + msg=msg ) dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) if dataset: # Copied from edit attributes for 'regular' datasets with some additions @@ -818,7 +840,8 @@ class Admin( BaseController ): datatypes=ldatatypes, dataset_actions=dataset_actions, gdas=gdas, - err=None ) + err=None, + msg=msg ) else: return trans.show_error_message( "Invalid dataset specified" ) def renderable( self, trans, component, group_id ): diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako index 28596e03fb3..d4d1a204d88 100644 --- a/templates/admin/dataset_security/group_create.mako +++ b/templates/admin/dataset_security/group_create.mako @@ -87,7 +87,7 @@ %endif -
    +
    diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako index 58406d7edbf..55c47da7f5a 100644 --- a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -64,7 +64,7 @@ <% ctr += 1 %> %endfor -
    +
    %endif diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako index c7defaa7697..48cc138474a 100644 --- a/templates/admin/dataset_security/group_members_edit.mako +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -105,7 +105,7 @@ %endif -
    +
    diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako index 7e44442e1aa..d4d2d1fccfd 100644 --- a/templates/admin/library/libraries.mako +++ b/templates/admin/library/libraries.mako @@ -8,6 +8,9 @@

    + %if msg: +

    ${msg}

    + %endif
    Galaxy Libraries
    %for library in libraries: From 8f19013837bb525995693837586fc0881b03f3e2 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 21 Aug 2008 13:20:48 -0400 Subject: [PATCH 25/94] Add a 'pencil' icon to datasets in the library import tool, allows users with appropriate permissions to edit metadata on datasets in the library. --- lib/galaxy/web/controllers/root.py | 27 +++++-- templates/dataset/edit_attributes.mako | 101 ++++++++++++++++--------- templates/library/library.mako | 13 +++- 3 files changed, 99 insertions(+), 42 deletions(-) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 83ab538e87f..1324499974b 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -203,29 +203,38 @@ class RootController( BaseController ): yield "No data with id=%d" % id @web.expose - def edit(self, trans, id=None, hid=None, **kwd): + def edit(self, trans, id=None, hid=None, lid=None, **kwd): """Returns data directly into the browser. Sets the mime-type according to the extension""" if hid is not None: history = trans.get_history() # TODO: hid handling data = history.datasets[ int( hid ) - 1 ] - elif id is None: - return trans.show_error_message( "Problem loading dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) - else: + elif lid is not None: + data = self.app.model.LibraryFolderDatasetAssociation.get( lid ) + elif id is not None: data = self.app.model.HistoryDatasetAssociation.get( id ) + else: + trans.log_event( "Problem loading dataset id %s with history id %s and library id %s." % ( str( id ), str( hid ), str( lid ) ) ) + return trans.show_error_message( "Problem loading dataset." ) if data is None: - return trans.show_error_message( "Problem retrieving dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) - if data.history.user is not None and data.history.user != trans.user: + trans.log_event( "Problem retrieving dataset id %s with history id %s and library id %s." % ( str( id ), str( hid ), str( lid ) ) ) + return trans.show_error_message( "Problem retrieving dataset." ) + if id is not None and 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) + can_edit_metadata = lid is None or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) if p.change: # The user clicked the Save button on the 'Change data type' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) 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 + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) data.name = p.name data.info = p.info @@ -245,6 +254,8 @@ class RootController( BaseController ): 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 + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) 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': @@ -255,7 +266,11 @@ class RootController( BaseController ): trans.app.model.flush() return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) elif p.convert_data: + if lid is not None: + return trans.show_error_message( "Data in the library cannot be converted. Please import it to a history and covert it." ) """The user clicked the Convert button on the 'Convert to new format' form""" + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) target_type = kwd.get("target_type", None) if target_type: msg = data.datatype.convert_dataset(trans, data, target_type) diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 712b2eccfc5..1ed13711513 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -15,11 +15,19 @@ +<% +if isinstance( data, trans.app.model.HistoryDatasetAssociation ): + id_name = 'id' +elif isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): + id_name = 'lid' +%> + +%if ( id_name == 'id' or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) ):
    Edit Attributes
    - +
    - +
    @@ -68,39 +76,41 @@

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

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

    + +

    +
    + +

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

    Change data type
    - +

    +%else: +

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

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

    +
    Change Dataset Access Permissions
    +
    + + +
    + <% groups = trans.app.model.Group.select() %> + <% active_group_ids = [ assoc.group.id for assoc in datasets[0].dataset.groups ] %> +
    + Check each group which should have access to this dataset. +
    + %for group in groups: + %if group.id in active_group_ids: + <% assoc = filter( lambda x: x.group_id == group.id, datasets[0].dataset.groups )[0] %> + %else: + <% assoc = None %> + %endif + ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor + +
    + +
    +
    diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index a7877e54479..aae1e1c1a2f 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -51,6 +51,7 @@
    +

    Edit Attributes
    diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 1c335678ffb..0d5a87966bd 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -1,5 +1,7 @@ <%inherit file="/base.mako"/> +<% import os %> + <%def name="title()">Create New Library Dataset
    @@ -27,6 +29,23 @@
    + %if trans.app.config.library_import_dir is not None: +
    + +
    + +
    +
    + You may also choose to upload all files in a subdirectory of ${trans.app.config.library_import_dir} on the Galaxy server. +
    +
    +
    + %endif
    Yes
    diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 58ca578eb15..4930c93ec6e 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -80,6 +80,10 @@ use_interactive = true # Admin Users - this should be a comma-separated list of valid Galaxy users #admin_users = user1@bx.psu.edu,user2@bx.psu.edu +# Files in directories under this directory can be directly imported through +# the library admin's "add dataset" tool +## library_import_dir = /var/opt/galaxy/import + # path to sendmail sendmail_path = /usr/sbin/sendmail From a0f38f5595e7d5cfef0eada2de9b284445f0baf5 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 28 Aug 2008 14:01:03 -0400 Subject: [PATCH 39/94] Copy gzip handling from the upload tool to the admin library import tool. Speaking of, we should probably find a way to combine the two, since they basically do the same thing. There is a lot of duplicated code here. --- lib/galaxy/web/controllers/admin.py | 48 +++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index b74734921ca..9ca06a42948 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,6 +1,5 @@ - -import shutil, StringIO, operator, urllib -from galaxy import util +import shutil, StringIO, operator, urllib, gzip, tempfile +from galaxy import util, datatypes from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent @@ -648,6 +647,33 @@ class Admin( BaseController ): 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 ) + + # 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 uncompress the temp_name file + CHUNK_SIZE = 2**20 # 1Mb + fd, uncompressed = tempfile.mkstemp() + gzipped_file = gzip.GzipFile( temp_name ) + while 1: + try: + chunk = gzipped_file.read( CHUNK_SIZE ) + except IOError: + os.close( fd ) + os.remove( uncompressed ) + raise BadFileException( 'problem decompressing gzipped data.' ) + if not chunk: + break + os.write( fd, chunk ) + os.close( fd ) + gzipped_file.close() + # Replace the gzipped file with the decompressed file + shutil.move( uncompressed, temp_name ) + name = name.rstrip( '.gz' ) + data_type = 'gzip' + if space_to_tab: line_count = sniff.convert_newlines_sep2tabs( temp_name ) else: @@ -926,6 +952,22 @@ class Admin( BaseController ): msg=msg ) else: return trans.show_error_message( "Invalid dataset specified" ) + def check_gzip( self, temp_name ): + """ + Utility method to check gzipped uploads + """ + temp = open( temp_name, "U" ) + magic_check = temp.read( 2 ) + temp.close() + if magic_check != datatypes.data.gzip_magic: + return ( False, False ) + CHUNK_SIZE = 2**15 # 32Kb + gzipped_file = gzip.GzipFile( temp_name ) + chunk = gzipped_file.read( CHUNK_SIZE ) + gzipped_file.close() + #if self.check_html( temp_name, chunk=chunk ) or self.check_binary( temp_name, chunk=chunk ): + # return( True, False ) + return ( True, True ) @web.expose def change_permissions( self, trans, ids=[], **kwd ): if not self.user_is_admin( trans ): From ff321b14691979e8f7ba99a7429120cf11d5e29c Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 29 Aug 2008 13:50:58 -0400 Subject: [PATCH 40/94] When uploading to the library, make permissions options a bit clearer. --- lib/galaxy/web/controllers/admin.py | 29 ++++++++------- templates/admin/library/dataset.mako | 47 ++++++++++-------------- templates/admin/library/new_dataset.mako | 26 ++++++++++--- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 9ca06a42948..fce461c71ca 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -643,6 +643,12 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg + def listify( item ): + if isinstance( item, list ): + return item + else: + return [ item ] + # 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 @@ -731,22 +737,19 @@ class Admin( BaseController ): if 'space_to_tab' in kwd: if kwd['space_to_tab'] not in ["None", None]: space_to_tab = True - if 'groups' not in kwd: - msg = 'The dataset must be associated with at least 1 group.' + if 'groups' not in kwd and 'users' not in kwd and 'public' not in kwd: + msg = 'The dataset must be associated with at least 1 user or group, or be set public.' trans.response.send_redirect( web.url_for( action='dataset', folder_id=folder_id, msg=msg ) ) - groups = kwd['groups'] - if groups and not isinstance( groups, list ): - # mako sends singleton lists as a string - groups = [ groups ] - elif groups is None: - groups = [] - # Greg: what's this for? it kills the ability to select multiple groups - #else: - # groups = [] + groups = [] + if 'groups' in kwd: + groups.extend( listify(kwd['groups']) ) + if 'users' in kwd: + groups.extend( listify(kwd['users']) ) + if 'public' in kwd: + groups.extend( [ trans.app.security_agent.get_public_group().id ] ) temp_name = "" data_list = [] created_datasets = [] - if 'filename' in dir( data_file ): file_name = data_file.filename file_name = file_name.split( '\\' )[-1] @@ -843,7 +846,7 @@ class Admin( BaseController ): # 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 + # The user clicked the Save button on the 'Dataset Permissions' form actions = p.actions if actions and not isinstance( actions, list ): actions = [ actions ] diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index aae1e1c1a2f..ea17090632f 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -15,38 +15,29 @@ -<%def name="group_dataset_permitted_actions( dataset_actions, gda )"> - %for da in dataset_actions: - <% check = False %> - %for action in gda[2]: - %if action == da: - <% - check = True - break - %> - %endif - %endfor - %if check: - - %else: - - %endif - ${da}
    - %endfor - -
    -
    Group Associations
    +
    Dataset Permissions
    - %for gda in gdas: -
    ${gda[1]}
    -
    -
    - ${group_dataset_permitted_actions( dataset_actions, gda )} -
    - %endfor +
    + <% dataset_gdas = [ assoc for assoc in dataset.dataset.groups ] %> +
    + Choose the permissions each user or group should have on this dataset. +
    + %for gda in dataset_gdas: + ${gda.group.name.replace( ' private group', '' )}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor +
    diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 0d5a87966bd..1154e3af8bf 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -85,14 +85,30 @@
    - - Multi-select list - hold the appropriate key while clicking to select multiple columns + + This dataset can be accessed by anyone (make it public).
    +

    + <% user_groups = [ g for g in groups if g[1].endswith( ' private group' ) ] %> + <% real_groups = [ g for g in groups if not g[1].endswith( ' private group' ) and g[1] != 'public' ] %>

    - + %for group in user_groups: + %endfor + %if len( real_groups ): +

    + + + %endif +

    +
    + To select multiple users or groups, hold ctrl or command while clicking.
    From 9be266362bf1458f73a1f88d08f7c69b598bf25f Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 3 Sep 2008 12:14:26 -0400 Subject: [PATCH 41/94] You can now update individual dataset permissions from a group's dataset list in the admin interface. --- lib/galaxy/security/__init__.py | 7 +- lib/galaxy/web/controllers/admin.py | 70 +++++++------------ .../group_dataset_permitted_actions_edit.mako | 65 ++++++++--------- 3 files changed, 64 insertions(+), 78 deletions(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index daf85db4318..d248c37d990 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -235,10 +235,13 @@ class GalaxyRBACAgent( RBACAgent ): permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] for ptuple in permissions: self.associate_components( dataset=dataset, permissions=ptuple ) - def get_dataset_permissions( self, dataset ): + def get_dataset_permissions( self, dataset, group_id=None ): if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset - return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups ] + if group_id is not None: + return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups if gda.group_id == int(group_id) ][0] + else: + return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups ] def get_component_associations( self, **kwd ): # TODO, Nate: Make sure this method is functionally correct. assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index fce461c71ca..63703b57ca7 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -284,44 +284,12 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg group_id = int( params.group_id ) - group_name = unescape( params.group_name, unentities ) gdas = [] permitted_actions = [] - # Need to get all actions to send to the form - dataset_actions = [] - dpas = RBACAgent.permitted_actions - dpa_descriptions = RBACAgent.permitted_action_descriptions - for dpa in dpas.items(): - pa = pa_description = '' - pa = dpa[0] - if dpa[0].startswith( 'DATASET' ): - for dpa_description in dpa_descriptions.items(): - if pa == dpa_description[0]: - pa = dpa[1] - pa_description = dpa_description[1] - break - dataset_actions.append( ( pa, pa_description ) ) - dataset_actions = sorted( dataset_actions, key=operator.itemgetter(0) ) - 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.group_id == group_id, - from_obj = [ sa.outerjoin( galaxy.model.Group.table, - galaxy.model.GroupDatasetAssociation.table ) ] ) - 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 + group = galaxy.model.Group.get( group_id ) 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, + group=group, + gdas=group.datasets, msg=msg ) @web.expose def group_dataset_permitted_actions_edit( self, trans, **kwd ): @@ -329,15 +297,29 @@ class Admin( BaseController ): 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 + permissions = {} + for gda_id in params.gdas.split(','): + permissions[gda_id] = params.get('gda_actions_%s' % gda_id, None) + if isinstance( permissions[gda_id], str ): + permissions[gda_id] = [ permissions[gda_id] ] + for gda_id, permitted_actions in permissions.items(): + if permitted_actions is None: + gda = trans.app.model.GroupDatasetAssociation.get( int( gda_id ) ) + if gda.permitted_actions is not []: + log.debug( "gda %s: permitted_actions emptied (set to none)" % gda_id ) + gda.permitted_actions = [] + gda.flush() + else: + valid_pas = trans.app.model.security_agent.convert_permitted_action_strings( permitted_actions ) + if not valid_pas: + continue # this shouldn't happen, but might as well check + gda = trans.app.model.GroupDatasetAssociation.get( int( gda_id ) ) + if sorted(gda.permitted_actions) != sorted(valid_pas): + log.debug( "gda %s: permitted_actions changed to %s" % ( gda_id, valid_pas ) ) + gda = trans.app.model.GroupDatasetAssociation.get( int( gda_id ) ) + gda.permitted_actions = valid_pas + gda.flush() + msg = "The dataset permitted actions have been updated" trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) @web.expose def mark_group_deleted( self, trans, **kwd ): diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako index 6648a72446f..5afe2ae809f 100644 --- a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -1,12 +1,5 @@ <%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
    @@ -14,50 +7,58 @@ Groups  |   Users
    -

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

    +

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

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

    ${msg}

     
    There is no Galaxy group named '${gn}'
    The group you selected has no associated datasets.
    GroupPriorityPermitted Actions on DatasetsAssociation Names/InfoPermitted Actions
    ${gn}${gda[0]} - %for da in dataset_actions: - <% check = False %> - %for action in gda[1]: - %if action == da[0]: - <% - check = True - break - %> - %endif - %endfor - %if check: - - %else: - + %if gda.dataset.library_associations: + Library name(s):
    + %endif +
      + %for da in gda.dataset.library_associations: +
    • ${da.name} (${da.info})
    • + %endfor +
    + %if gda.dataset.history_associations: + History name(s):
    + %endif +
      + %for da in gda.dataset.history_associations: +
    • ${da.name} (${da.info})
    • + %endfor +
    +
    + %for pa in trans.app.model.Dataset.permitted_actions: + <% pa_val = trans.app.security_agent.permitted_actions.__dict__[pa] %> + ${da[1]}
    + /> + ${pa_val}
    ${trans.app.security_agent.get_permitted_action_description(pa)}

    %endfor
    From 85d1203a5cc99a4a1eac188b9103c7f6bccbeb70 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 3 Sep 2008 12:27:41 -0400 Subject: [PATCH 42/94] Tab-like top level navigation. This makes the "workflow" view separate from the "interactive analysis" view, which makes sense since tools and history are irrelevant when managing workflows. The "workflow" entry in the tool menu now only shows up if a user has workflows added to it, otherwise you get to the workflow management screen from the top. Adds infrastructure for a few things (including pages with 0, 1, or both panels, a compact message box). --- lib/galaxy/web/controllers/root.py | 4 +- lib/galaxy/web/controllers/workflow.py | 8 +- static/june_2007_style/blue/base.css | 1 + static/june_2007_style/blue/masthead.css | 36 ++++++ static/june_2007_style/blue/panel_layout.css | 54 +++++++-- static/june_2007_style/blue_colors.ini | 1 + static/june_2007_style/make_style.py | 5 +- static/june_2007_style/masthead.css.tmpl | 36 ++++++ static/june_2007_style/panel_layout.css.tmpl | 46 ++++++++ templates/base_panels.mako | 56 +++++++++- templates/root/index.mako | 8 ++ templates/root/masthead.mako | 38 +++++-- templates/root/tool_menu.mako | 25 ++--- templates/workflow/editor.mako | 28 +++-- templates/workflow/index.mako | 112 +++---------------- templates/workflow/list.mako | 99 ++++++++++++++++ 16 files changed, 408 insertions(+), 149 deletions(-) create mode 100644 templates/workflow/list.mako diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 1324499974b..aff793e79cf 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -671,7 +671,7 @@ class RootController( BaseController ): return trans.show_error_message( "

    Failed to make secondary dataset primary.

    " ) @web.expose - def masthead( self, trans ): + def masthead( self, trans, active_view=None ): brand = trans.app.config.get( "brand", "" ) if brand: brand ="/%s" % brand @@ -687,7 +687,7 @@ class RootController( BaseController ): 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, admin_user=admin_user ) + blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url, admin_user=admin_user, active_view=active_view ) @web.expose def dataset_errors( self, trans, id=None, **kwd ): diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index b33a051f350..502b9cdc8e7 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -16,8 +16,12 @@ class WorkflowController( BaseController ): beta = True @web.expose - @web.require_login( "use Galaxy workflows" ) def index( self, trans ): + return trans.fill_template( "workflow/index.mako" ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def list( self, trans ): """ Render workflow main page (management of existing workflows) """ @@ -32,7 +36,7 @@ class WorkflowController( BaseController ): .filter( model.StoredWorkflow.c.deleted == False ) \ .order_by( desc( model.StoredWorkflow.c.update_time ) ) \ .all() - return trans.fill_template( "workflow/index.mako", + return trans.fill_template( "workflow/list.mako", workflows = workflows, shared_by_others = shared_by_others ) diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 418362d4610..6e2b8ee1b20 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -16,6 +16,7 @@ img border: 0; } + a:link, a:visited, a:active { color: #303030; diff --git a/static/june_2007_style/blue/masthead.css b/static/june_2007_style/blue/masthead.css index f206e7f1b44..b89faddb0a8 100644 --- a/static/june_2007_style/blue/masthead.css +++ b/static/june_2007_style/blue/masthead.css @@ -7,6 +7,7 @@ body margin: 3px; margin-right: 5px; margin-left: 5px; + overflow: hidden; } div.pageTitle @@ -24,3 +25,38 @@ a:link, a:visited, a:active { color: #eeeeee; } + +#tab-bar-bottom +{ + z-index: -1; + position:absolute; + top:27px; left: 0; + width: 100%; + height: 100%; + background: #222532; +} + +span.link-group +{ + margin: 0; + padding: 0; + display: inline; + padding-bottom: 10px; + margin-bottom: -10px; +} + +span.link-group span +{ + margin: 0; + padding: 0; + display: inline; +} + +span.link-group span.active-link +{ + background: #222532; + padding-left: 3px; padding-right: 3px; + margin-left: -3px; margin-right: -3px; + padding-bottom: 10px; + margin-bottom: -10px; +} \ No newline at end of file diff --git a/static/june_2007_style/blue/panel_layout.css b/static/june_2007_style/blue/panel_layout.css index b7ec445bc43..7dda79706a8 100644 --- a/static/june_2007_style/blue/panel_layout.css +++ b/static/june_2007_style/blue/panel_layout.css @@ -45,6 +45,19 @@ body font-weight: bold; } +#messagebox +{ + position:absolute; + top:33px; + left:0; + width:100%; + height:24px !important; + overflow: hidden; + border-bottom: solid #999 1px; + font-size: 90%; +} + + #left, #left-border, #center, #right-border, #right { position: absolute; @@ -146,7 +159,6 @@ table.column-layout .unified-panel-header { height: 2em; - overflow: hidden; z-index: 1000; background: #cccccc; background-image: url(panel_header_bg.png); @@ -160,18 +172,17 @@ table.column-layout color: #333; font-weight: bold; } - + .unified-panel-header-inner { padding-top: 0.45em; } - + .menu-bg { background: #C1C9E5 url(menu_bg.png) top repeat-x; } div.unified-panel-body { position: absolute; - z-index: 1; top: 2em; bottom: 0; width: 100%; @@ -197,6 +208,7 @@ div.unified-panel-body { color: black; background: #aaaaaa; } + .panel-header-button:active { color: white; background: #aaaaaa; @@ -240,7 +252,7 @@ div.popupmenu-item:hover { } #overlay { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 20000; } @@ -271,6 +283,34 @@ div.popupmenu-item:hover { padding: 5px; } -.dialog-box body { - overflow: scroll; + +.panel-error-message, .panel-warning-message, .panel-done-message, .panel-info-message +{ + height: 24px; + line-height: 24px; + color: #303030; + padding: 0px; + padding-left: 26px; + background-color: #FFCCCC; + background-image: url(error_small.png); + background-repeat: no-repeat; + background-position: 6px 50%; +} + +.panel-warning-message +{ + background-image: url(warn_small.png); + background-color: #FFFFCC; +} + +.panel-done-message +{ + background-image: url(done_small.png); + background-color: #CCFFCC; +} + +.panel-info-message +{ + background-image: url(info_small.png); + background-color: #CCCCFF; } \ No newline at end of file diff --git a/static/june_2007_style/blue_colors.ini b/static/june_2007_style/blue_colors.ini index 447ca3f98bc..89c61a73016 100644 --- a/static/june_2007_style/blue_colors.ini +++ b/static/june_2007_style/blue_colors.ini @@ -48,6 +48,7 @@ masthead_bg=#2C3143 masthead_text=#eeeeee masthead_bg_hatch=- masthead_link=#eeeeee +masthead_active_tab_bg=#222532 # ---- Layout ----------------------------------------------------------------- # Overall background color (including space between panels) layout_bg=#eee diff --git a/static/june_2007_style/make_style.py b/static/june_2007_style/make_style.py index 4ba96651c52..74a52166332 100755 --- a/static/june_2007_style/make_style.py +++ b/static/june_2007_style/make_style.py @@ -13,7 +13,6 @@ def run( cmd ): templates = [ ( "base.css.tmpl", "base.css" ), ( "panel_layout.css.tmpl", "panel_layout.css" ), - ( "panel_layout_ie.css.tmpl", "panel_layout_ie.css" ), ( "masthead.css.tmpl", "masthead.css"), ( "history.css.tmpl", "history.css" ), ( "tool_menu.css.tmpl", "tool_menu.css" ), @@ -64,7 +63,8 @@ for line in open( vars ): for input, output in templates: print input ,"->", output open( os.path.join( out_dir, output ), "w" ).write( str( Template( file=input, searchList=[context] ) ) ) - + +""" for rule, output in images: t = string.Template( rule ).substitute( context ) print t, "->", output @@ -74,3 +74,4 @@ for src, bg, out in shared_images: t = "./png_over_color.py shared_images/%s %s %s" % ( src, context[bg], os.path.join( out_dir, out ) ) print t run( t.split() ) +""" \ No newline at end of file diff --git a/static/june_2007_style/masthead.css.tmpl b/static/june_2007_style/masthead.css.tmpl index 0c759f2aba6..a6d3b7ea5c7 100644 --- a/static/june_2007_style/masthead.css.tmpl +++ b/static/june_2007_style/masthead.css.tmpl @@ -7,6 +7,7 @@ body margin: 3px; margin-right: 5px; margin-left: 5px; + overflow: hidden; } div.pageTitle @@ -24,3 +25,38 @@ a:link, a:visited, a:active { color: $masthead_link; } + +#tab-bar-bottom +{ + z-index: -1; + position:absolute; + top:27px; left: 0; + width: 100%; + height: 100%; + background: $masthead_active_tab_bg; +} + +span.link-group +{ + margin: 0; + padding: 0; + display: inline; + padding-bottom: 10px; + margin-bottom: -10px; +} + +span.link-group span +{ + margin: 0; + padding: 0; + display: inline; +} + +span.link-group span.active-link +{ + background: $masthead_active_tab_bg; + padding-left: 3px; padding-right: 3px; + margin-left: -3px; margin-right: -3px; + padding-bottom: 10px; + margin-bottom: -10px; +} \ No newline at end of file diff --git a/static/june_2007_style/panel_layout.css.tmpl b/static/june_2007_style/panel_layout.css.tmpl index 0479767977b..4df44dba18a 100644 --- a/static/june_2007_style/panel_layout.css.tmpl +++ b/static/june_2007_style/panel_layout.css.tmpl @@ -45,6 +45,19 @@ body font-weight: bold; } +#messagebox +{ + position:absolute; + top:33px; + left:0; + width:100%; + height:24px !important; + overflow: hidden; + border-bottom: solid #999 1px; + font-size: 90%; +} + + #left, #left-border, #center, #right-border, #right { position: absolute; @@ -268,4 +281,37 @@ div.popupmenu-item:hover { .dialog-box .body, .dialog-box .buttons { padding: 5px; +} + +## Messages for message box, slightly different style + +.panel-error-message, .panel-warning-message, .panel-done-message, .panel-info-message +{ + height: 24px; + line-height: 24px; + color: $base_text; + padding: 0px; + padding-left: 26px; + background-color: $error_message_bg; + background-image: url(error_small.png); + background-repeat: no-repeat; + background-position: 6px 50%; +} + +.panel-warning-message +{ + background-image: url(warn_small.png); + background-color: $warn_message_bg; +} + +.panel-done-message +{ + background-image: url(done_small.png); + background-color: $done_message_bg; +} + +.panel-info-message +{ + background-image: url(info_small.png); + background-color: $info_message_bg; } \ No newline at end of file diff --git a/templates/base_panels.mako b/templates/base_panels.mako index 29a2dee470e..0d275ed44c2 100644 --- a/templates/base_panels.mako +++ b/templates/base_panels.mako @@ -1,6 +1,18 @@ ## This needs to be on the first line, otherwise IE6 goes into quirks mode +<% + self.has_left_panel=True + self.has_right_panel=True + self.message_box_visible=False + self.message_box_class="" + self.active_view=None +%> + +<%def name="init()"> +## Override + + ## Default title <%def name="title()">Galaxy @@ -8,6 +20,22 @@ <%def name="stylesheets()"> + ## Default javascripts @@ -27,19 +55,30 @@ ## Masthead <%def name="masthead()"> - + + + +## Messagebox +<%def name="message_box_content()"> ## Document + + ${self.init()} + ${self.title()} ${self.javascripts()} @@ -53,17 +92,26 @@
    ${self.masthead()}
    +
    + %if self.message_box_visible: + ${self.message_box_content()} + %endif +
    + %if self.has_left_panel:
    ${self.left_panel()}
    + %endif
    ${self.center_panel()}
    + %if self.has_right_panel:
    + %endif ## Allow other body level elements ${next.body()} diff --git a/templates/root/index.mako b/templates/root/index.mako index f573571162b..bcc6b3d8ee7 100644 --- a/templates/root/index.mako +++ b/templates/root/index.mako @@ -1,5 +1,13 @@ <%inherit file="/base_panels.mako"/> +<%def name="init()"> +<% + self.has_left_panel=True + self.has_right_panel=True + self.active_view="analysis" +%> + + <%def name="left_panel()">
    Tools
    diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 097d7707acc..b308158c941 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -9,35 +9,51 @@ - + +
    Galaxy${brand}
    - - Info: report bugs - | wiki - | screencasts - | blog + View: + analysis + | workflow + +     + + Info: report bugs + | wiki + | screencasts %if admin_user == "true": - | admin + | admin %endif +     + %if app.config.use_remote_user: Logged in as ${t.user.email} %else: %if t.user: - Logged in as ${t.user.email}: manage - | logout + Logged in as ${t.user.email}: manage + | logout %else: - Account: create - | login + Account: create + | login %endif %endif   +
    diff --git a/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index 431b2fffc43..2ae31c6f3fd 100644 --- a/templates/root/tool_menu.mako +++ b/templates/root/tool_menu.mako @@ -83,25 +83,22 @@ ## configure which of their stored workflows appear in the tools menu). %if app.config.enable_beta_features: -
    -
    -
    - Workflow (beta) -
    -
    -
    -
    - Manage workflows -
    - %if t.user: + %if t.user and t.user.stored_workflow_menu_entries: +
    +
    +
    + Your workflows +
    +
    +
    %for m in t.user.stored_workflow_menu_entries: %endfor - %endif -
    -
    +
    +
    + %endif %endif
    diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index 75b6c0c92e3..b7c523d6205 100644 --- a/templates/workflow/editor.mako +++ b/templates/workflow/editor.mako @@ -1,6 +1,18 @@ <%inherit file="/base_panels.mako"/> -<%def name="title()">Galaxy Workflow Editor +<%def name="init()"> +<% + self.active_view="workflow" + self.message_box_visible=True + self.message_box_class="warning" +%> + + +<%def name="message_box_content()"> + Workflow support is currently in beta testing. + Workflows may not work with all tools, may fail unexpectedly, and may + not be compatible with future updates to Galaxy. + <%def name="late_javascripts()"> @@ -217,7 +229,7 @@ } var close_editor = function() { - <% next_url = h.url_for( controller='root', m_c='workflow' ) %> + <% next_url = h.url_for( controller='workflow', action='index' ) %> if ( workflow && workflow.has_changes ) { do_close = function() { window.onbeforeunload = undefined; @@ -289,11 +301,12 @@ <%def name="stylesheets()"> - ${parent.stylesheets()} - - ## Also include "base.css" for styling tool menu and forms (details) + ## Include "base.css" for styling tool menu and forms (details) + ## But make sure styles for the layout take precedence + ${parent.stylesheets()} + + + +<%def name="left_panel()"> +
    +
    Administration
    +
    +
    +
    +
    +
    + Security +
    + +
    +
    + Data +
    + +
    +
    + Tools +
    + +
    +
    +
    + + +<%def name="center_panel()"> + + + + \ No newline at end of file diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index b308158c941..a28931773ad 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -27,15 +27,20 @@ class="active-link" %endif >workflow + %if admin_user == "true": + | admin + %endif +     Info: report bugs | wiki | screencasts - %if admin_user == "true": - | admin - %endif diff --git a/templates/workflow/list.mako b/templates/workflow/list.mako index 00ee3c3992c..a3d7affb50e 100644 --- a/templates/workflow/list.mako +++ b/templates/workflow/list.mako @@ -17,15 +17,17 @@

    Your workflows

    - + %if workflows: - +
    From dc049016d7684dc3894a3dd457ecbe70141603f9 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 3 Sep 2008 13:25:27 -0400 Subject: [PATCH 44/94] Add permission style to CSS template and regenerate. --- static/june_2007_style/base.css.tmpl | 6 +++++- static/june_2007_style/blue/base.css | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index 9dbb402d5b1..a26546f242c 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -426,4 +426,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 1b637ba530d..af6ec775ecd 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -425,4 +425,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} From fabaf9e645f04d481432af7eead85b034f630634 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 4 Sep 2008 10:32:23 -0400 Subject: [PATCH 45/94] Eliminate unique restriction on library name - requires db schema change: drop index ix_library_name; create index ix_library_name on library(name); --- lib/galaxy/model/mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 20c9f567bbf..5beab407c4c 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -182,7 +182,7 @@ Library.table = Table( "library", metadata, Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), - Column( "name", TEXT, index=True, unique=True ), + Column( "name", TEXT, index=True ), Column( "deleted", Boolean, index=True, default=False ), Column( "description", TEXT ) ) From f05de947a151a27e05f810fa189f4e94c8a45726 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Thu, 4 Sep 2008 13:22:37 -0400 Subject: [PATCH 46/94] Add a flag to datatypes 'copy_safe_peek', which when False will cause the peek to be rengenerated when a dataset is copied. Peeks such as for GMAJ need to be generated uniquely for each dataset occurance (dataset_id is needed for viewing, history_id is needed for exporting from gmaj). --- lib/galaxy/datatypes/data.py | 3 ++ lib/galaxy/datatypes/images.py | 50 ++++++++++++++++----------- lib/galaxy/model/__init__.py | 33 ++++++++++++------ lib/galaxy/web/controllers/admin.py | 2 +- lib/galaxy/web/controllers/library.py | 4 +-- 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py index 071e7ce53a7..53a8bca7ba0 100644 --- a/lib/galaxy/datatypes/data.py +++ b/lib/galaxy/datatypes/data.py @@ -47,6 +47,9 @@ class Data( object ): """Stores the set of display applications, and viewing methods, supported by this datatype """ supported_display_apps = {} + """If False, the peek is regenerated whenever a dataset of this type is copied""" + copy_safe_peek = True + def __init__(self, **kwd): """Initialize the datatype""" object.__init__(self, **kwd) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 7c58d38f632..1db5291a5a2 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -103,18 +103,22 @@ def create_applet_tag_peek( class_name, archive, params ): class Gmaj( data.Data ): """Class describing a GMAJ Applet""" file_ext = "gmaj.zip" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, - "buttonlabel": "Launch GMAJ", - "nobutton": "false", - "urlpause" :"100", - "debug": "false", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ) - } - class_name = "edu.psu.bx.gmaj.MajApplet.class" - archive = "/static/gmaj/gmaj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, + "buttonlabel": "Launch GMAJ", + "nobutton": "false", + "urlpause" :"100", + "debug": "false", + "posturl": "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" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the GMAJ applet." dataset.blurb = 'GMAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: @@ -175,17 +179,21 @@ class Html( data.Text ): class Laj( data.Text ): """Class describing a LAJ Applet""" file_ext = "laj" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "alignfile1": "display?id=%s" % dataset.id, - "buttonlabel": "Launch LAJ", - "title": "LAJ in Galaxy", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ), - "noseq": "true" - } - class_name = "edu.psu.cse.bio.laj.LajApplet.class" - archive = "/static/laj/laj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "alignfile1": "display?id=%s" % dataset.id, + "buttonlabel": "Launch LAJ", + "title": "LAJ in Galaxy", + "posturl": "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" + archive = "/static/laj/laj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the LAJ applet." dataset.blurb = 'LAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index d9ed17b1aa5..0116b26ec2c 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -413,7 +413,7 @@ class HistoryDatasetAssociation( DatasetInstance ): 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 ): + def copy( self, copy_children = False, parent_id = None, target_history = None ): des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, @@ -426,12 +426,14 @@ class HistoryDatasetAssociation( DatasetInstance ): visible=self.visible, deleted=self.deleted, parent_id=parent_id, - copied_from_history_dataset_association=self ) + copied_from_history_dataset_association=self, + history = target_history ) des.flush() if copy_children: for child in self.children: child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs des.flush() return des def clear_associated_files( self, metadata_safe = False, purge = False ): @@ -493,8 +495,8 @@ class History( object ): 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 = data.copy( copy_children = True, target_history = des ) + des.add_dataset( new_data, set_hid = False ) new_data.flush() des.hid_counter = self.hid_counter des.flush() @@ -540,7 +542,11 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 ): + def to_history_dataset_association( self, parent_id = None, target_history = None ): + if target_history: + hid = target_history._next_hid() + else: + hid = None des = HistoryDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, @@ -552,14 +558,17 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): visible=self.visible, deleted=self.deleted, parent_id=parent_id, - copied_from_library_folder_dataset_association = self ) + copied_from_library_folder_dataset_association = self, + history = target_history, + hid = hid ) des.flush() for child in self.children: child_copy = child.to_history_dataset_association( parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs des.flush() return des - def copy( self, copy_children = False, parent_id = None ): + def copy( self, copy_children = False, parent_id = None, target_folder = None ): des = LibraryFolderDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, @@ -571,12 +580,14 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): visible=self.visible, deleted=self.deleted, parent_id=parent_id, - copied_from_library_folder_dataset_association = self ) + copied_from_library_folder_dataset_association = self, + folder = target_folder ) des.flush() if copy_children: for child in self.children: child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs des.flush() return des def clear_associated_files( self, metadata_safe = False, purge = False ): diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 63703b57ca7..f6608cfc879 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -708,7 +708,7 @@ class Admin( BaseController ): last_dataset_created = None data_file = kwd['file_data'] url_paste = kwd['url_paste'] - server_dir = kwd['server_dir'] + server_dir = kwd.get( 'server_dir', 'None' ) if data_file == '' and url_paste == '' and server_dir in [ 'None', '' ]: if trans.app.config.library_import_dir is not None: msg = 'Select a file, enter a URL or Text, or select a server directory.' diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 841830035ec..19820ebba10 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -32,8 +32,8 @@ class Library( BaseController ): 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 = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association( target_history = history ) + history.add_dataset( dataset, set_hid = not dataset.hid ) dataset.flush() history.flush() return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) From bd1a4e01ccc494e3357afb14c5195fa0afcf6f54 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 8 Sep 2008 13:41:55 -0400 Subject: [PATCH 47/94] More cleanup for admin UI. --- static/june_2007_style/base.css.tmpl | 5 ++ static/june_2007_style/blue/base.css | 7 ++- static/june_2007_style/blue_colors.ini | 2 +- .../admin/dataset_security/group_members.mako | 57 ++++++++++--------- .../dataset_security/group_members_edit.mako | 22 ++++--- templates/admin/dataset_security/groups.mako | 15 ++--- .../specified_users_groups.mako | 36 ++++++------ templates/admin/dataset_security/users.mako | 40 +++++-------- templates/admin/library/libraries.mako | 50 +++++++++------- templates/admin/library/library.mako | 17 +++--- templates/admin/reload_tool.mako | 33 +++++------ 11 files changed, 146 insertions(+), 138 deletions(-) diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index a26546f242c..28a0f87f546 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -283,6 +283,11 @@ table.colored tr background: $table_row_bg; } +table.colored tr.odd_row +{ + background: $odd_row_bg; +} + div.debug { margin: 10px; diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index af6ec775ecd..74a7b7c68ec 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -283,6 +283,11 @@ table.colored tr background: white; } +table.colored tr.odd_row +{ + background: #DADFEF; +} + div.debug { margin: 10px; @@ -294,7 +299,7 @@ div.debug div.odd_row { - background: #FFFF99; + background: #DADFEF; } #footer { diff --git a/static/june_2007_style/blue_colors.ini b/static/june_2007_style/blue_colors.ini index 89c61a73016..5b9f320b296 100644 --- a/static/june_2007_style/blue_colors.ini +++ b/static/june_2007_style/blue_colors.ini @@ -15,7 +15,7 @@ form_border=#d8b365 #form_body_bg=#FFFFFF form_body_bg_top=#FFFFFF form_body_bg_bottom=#FFFFFF -odd_row_bg=#FFFF99 +odd_row_bg=#DADFEF # Messages error_message_border=#AA6666 error_message_bg=#FFCCCC diff --git a/templates/admin/dataset_security/group_members.mako b/templates/admin/dataset_security/group_members.mako index 79c44284459..9acd9faafe2 100644 --- a/templates/admin/dataset_security/group_members.mako +++ b/templates/admin/dataset_security/group_members.mako @@ -7,32 +7,33 @@ <% gn = unescape( group_name, unentities ) %> -<%def name="title()">Create Group -
    -
    - Libraries  |   - %if deleted: - Deleted Groups  |   - %else: - Groups  |   - %endif -
    - - %if not deleted: - - %endif -
    Members of Group '${gn}'
    -
    Name # of Steps
    Users
    - %if msg: - - %endif - - %if len( members ) == 0: - - %else: +<%def name="title()">Group Members + +%if msg: +
    ${msg}
    +%endif + +

    Members of Group '${gn}'

    + +%if not deleted: + + +%endif + +%if len( members ) == 0: + + Group '${gn}' contains no members + +%else: + +

    ${msg}

     
    Group '${gn}' contains no members
    + <% ctr = 0 %> %for member in members: <% email = unescape( member[1], unentities ) %> @@ -48,6 +49,6 @@ <% ctr += 1 %> %endfor - %endif
    Username
    - + +%endif diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako index 48cc138474a..03ce26ded29 100644 --- a/templates/admin/dataset_security/group_members_edit.mako +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -6,17 +6,16 @@ %> <%def name="title()">Manage Group Membership -
    -
    - Libraries  |   - Groups  |   -
    Users
    - %if msg: - - %endif - <% gn = unescape( group_name, unentities ) %> + +%if msg: +
    ${msg}
    +%endif + +<% gn = unescape( group_name, unentities ) %> + +

    Members of group '${gn}'

    + +

    ${msg}

    %else: - - + %endif diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako index e5678635c96..546842b59f4 100644 --- a/templates/admin/dataset_security/specified_users_groups.mako +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -7,21 +7,22 @@ <% email = unescape( user_email, unentities ) %> -<%def name="title()">Create Group -
    -
    - Libraries  |   - Groups  |   -
    - -

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

    -
     
    @@ -25,7 +24,6 @@ %if len( users ) == 0:
    There are no Galaxy users
    Members of '${gn}' - Quick Find
    |A|B|C|D|E|F diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako index 17ebbd1d524..63a3920684a 100644 --- a/templates/admin/dataset_security/groups.mako +++ b/templates/admin/dataset_security/groups.mako @@ -64,15 +64,12 @@ anchored = False curr_anchor = 'A' %> -
    -
    - |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 -
    +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor
    Users
    - %if msg: - - %endif - %if len( groups ) == 0: - - %else: +<%def name="title()">Group Membership + +%if msg: +
    ${msg}
    +%endif + +

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

    + +%if len( groups ) == 0: + + User '${email}' belongs to no groups + +%else: + +

    ${msg}

    User '${email}' belongs to no groups
    + @@ -65,6 +66,7 @@ <% ctr += 1 %> %endfor - %endif +
    Group Priority
    -
    + +%endif diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako index 05c4d8e8455..12e1b4ca550 100644 --- a/templates/admin/dataset_security/users.mako +++ b/templates/admin/dataset_security/users.mako @@ -19,7 +19,6 @@ <% render_quick_find = len( users ) > 50 - ctr = 0 %> %if render_quick_find: <% @@ -28,34 +27,31 @@ anchored = False curr_anchor = 'A' %> - - + - %else: - %endif - %for user in users: + + %for ctr, user in enumerate( users ): <% email = unescape( user[1], unentities ) %> %if render_quick_find and not email.upper().startswith( curr_anchor ): <% anchored = False %> %endif - - %if ctr % 2 == 1: -
    - %endif +
    - %if ctr % 2 == 1: - - %endif - <% ctr += 1 %> %endfor
    - |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 +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor
    Email address
    Email address
    %if render_quick_find and email.upper().startswith( curr_anchor ): %if not anchored: -
    - -
    + <% anchored = True %> %endif ${email} @@ -64,9 +60,7 @@ %if email.upper().startswith( anchor ): %if not anchored: -
    - -
    + <% curr_anchor = anchor anchored = True @@ -83,10 +77,6 @@ ${email} %endif
    diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako index d4d2d1fccfd..3511b32a923 100644 --- a/templates/admin/library/libraries.mako +++ b/templates/admin/library/libraries.mako @@ -1,22 +1,34 @@ <%inherit file="/base.mako"/> <%def name="title()">Libraries -
    -
    - Groups  |   - Users -
    - -
    - %if msg: -

    ${msg}

    - %endif -
    Galaxy Libraries
    -
    - %for library in libraries: - - %endfor -
    -
    + +%if msg: +
    ${msg}
    +%endif + +

    Libraries

    + + + +%if len(libraries) == 0: + + There are no libraries + +%else: + + + + + + + %for library in libraries: + + + + + %endfor +
    NameDescription
    ${library.name}${library.description}
    + +%endif \ No newline at end of file diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako index 67db5c1d1f6..781a8b584ae 100644 --- a/templates/admin/library/library.mako +++ b/templates/admin/library/library.mako @@ -1,16 +1,13 @@ <%inherit file="/base.mako"/> <%def name="title()">Library + +%if msg: +
    ${msg}
    +%endif +
    -
    - Libraries  |   - Groups  |   - Users -
    - %if msg: -

    ${msg}

    - %endif -
    Manage Library '${library.name}'
    +
    Library '${library.name}'
    @@ -29,7 +26,7 @@
    - +
     
     
    diff --git a/templates/admin/reload_tool.mako b/templates/admin/reload_tool.mako index 1d050ea4764..3b8e568cfd4 100644 --- a/templates/admin/reload_tool.mako +++ b/templates/admin/reload_tool.mako @@ -1,16 +1,17 @@ <%inherit file="/base.mako"/> -
    -

    Reload a Tool

    - - %if msg: - - %endif - - - -

    ${msg}

    -
    -

    - Reload tool: +%if msg: +

    ${msg}
    +%endif + +
    +
    Reload Tool
    +
    + +
    + +
    +
    -

    - -
    +
    + +
    From 8266801ea01bb3a582a46b14f06a91e5607e60a1 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 8 Sep 2008 14:27:56 -0400 Subject: [PATCH 48/94] Fix for admin interface functional test. --- test/functional/test_security_and_libraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 30b460a049e..748684e696f 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -41,7 +41,7 @@ class TestHistory( TwillTestCase ): """Testing logging in as an admin user""" self.login( email='test@bx.psu.edu' ) #This is configured as our admin user self.visit_page( "admin" ) - self.check_page_for_string( 'Galaxy Administration' ) + self.check_page_for_string( 'Administration' ) self.logout() # Need to ensure that we have 2 users self.login( email='test2@bx.psu.edu' ) # This will not be an admin user From 4edc6d66263aeff227985a9f98165c59843a543a Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 10 Sep 2008 10:10:13 -0400 Subject: [PATCH 49/94] Spiffy new library browser interface. Also, dim datasets in your history on which you don't have permissions. --- LICENSE.txt | 7 +- lib/galaxy/security/__init__.py | 36 ++++++-- lib/galaxy/web/controllers/library.py | 65 ++++---------- static/images/expander_closed.png | Bin 0 -> 395 bytes static/images/expander_open.png | Bin 0 -> 446 bytes static/images/folder_closed.png | Bin 0 -> 537 bytes static/images/folder_open.png | Bin 0 -> 688 bytes static/images/library_closed.png | Bin 0 -> 593 bytes static/images/library_open.png | Bin 0 -> 622 bytes static/june_2007_style/blue/history.css | 9 +- static/june_2007_style/blue/library.css | 65 ++++++++++++++ static/june_2007_style/history.css.tmpl | 9 +- static/june_2007_style/library.css.tmpl | 65 ++++++++++++++ static/june_2007_style/make_style.py | 7 +- templates/library/browser.mako | 104 ++++++++++++++++++++++ templates/library/common.mako | 109 ++++++++++++++++++++++++ templates/library/libraries.mako | 13 --- templates/library/library.mako | 79 ----------------- templates/root/history_common.mako | 6 +- tool_conf.xml.sample | 2 +- tools/data_source/access_libraries.xml | 4 +- 21 files changed, 425 insertions(+), 155 deletions(-) create mode 100644 static/images/expander_closed.png create mode 100644 static/images/expander_open.png create mode 100644 static/images/folder_closed.png create mode 100644 static/images/folder_open.png create mode 100644 static/images/library_closed.png create mode 100644 static/images/library_open.png create mode 100644 static/june_2007_style/blue/library.css create mode 100644 static/june_2007_style/library.css.tmpl create mode 100644 templates/library/browser.mako create mode 100644 templates/library/common.mako delete mode 100644 templates/library/libraries.mako delete mode 100644 templates/library/library.mako diff --git a/LICENSE.txt b/LICENSE.txt index 69978e57efb..23e45c00eae 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -17,4 +17,9 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Some icons found in Galaxy are from the Silk Icons set, available under +the Creative Commons Attribution 2.5 License, from: + +http://www.famfamfam.com/lab/icons/silk/ diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index d248c37d990..6782bbe3089 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -89,11 +89,14 @@ class GalaxyRBACAgent( RBACAgent ): """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 + if user is None: + try: + public_assoc = [ gda for gda in dataset.groups if gda.group == self.get_public_group() ][0] + except: + # There'll be an IndexError if the dataset doesn't have a gda with public + return False + if action in public_assoc.permitted_actions: + return True elif user is not None: # Loop through permitted_actions and if allowed return true: # Check permitted_actions associated with dataset through groups @@ -231,7 +234,7 @@ class GalaxyRBACAgent( RBACAgent ): for group_dataset_assoc in dataset.groups: group_dataset_assoc.delete() group_dataset_assoc.flush() - if isinstance( permissions[0], self.model.GroupDatasetAssociation ): + if len( permissions ) and isinstance( permissions[0], self.model.GroupDatasetAssociation ): permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] for ptuple in permissions: self.associate_components( dataset=dataset, permissions=ptuple ) @@ -254,6 +257,27 @@ class GalaxyRBACAgent( RBACAgent ): raise 'No valid method of associating provided components: %s' % kwd def dataset_has_group( self, dataset_id, group_id ): return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) + def check_folder_contents( self, user, entry ): + """ + Return true if there are any datasets under 'folder' that the + user has access permission on. We do this a lot and it's a + pretty inefficient method, optimizations are welcomed. + """ + if isinstance( entry, self.model.Library ): + return self.check_folder_contents( user, entry.root_folder ) + elif isinstance( entry, self.model.LibraryFolderDatasetAssociation ): + return self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=entry ) + elif isinstance( entry, self.model.LibraryFolder ): + for dataset in entry.active_datasets: + if self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=dataset ): + return True + for folder in entry.active_folders: + if self.check_folder_contents( user, folder ): + return True + return False + else: + raise 'Passed an illegal object to check_folder_contents: %s' % type( entry ) + def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 19820ebba10..1d42735603d 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -6,52 +6,19 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose - def index( self, trans, library_id=None, import_ids=[], **kwd ): - def renderable( component, group_ids ): - #return True if component or at least one of components contents is - #associated with a group that is in group_ids - if isinstance( component, trans.app.model.LibraryFolder ): - # Check the folder's datasets to see what can be rendered - for library_folder_dataset_assoc in component.active_datasets: - if renderable( library_folder_dataset_assoc, group_ids ): - return True - # Check the folder's sub-folders to see what can be rendered - for library_folder in component.active_folders: - if renderable( library_folder, group_ids ): - return True - elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - 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: - return True - return False - - 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( target_history = history ) - history.add_dataset( dataset, set_hid = not dataset.hid ) - dataset.flush() - history.flush() - return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) - else: - # Need user to get associated Groups and Datasets - user = trans.get_user() - if user: - group_ids = [ user_group_assoc.group_id for user_group_assoc in user.groups ] - else: - group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] - - if 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. - library = trans.app.model.Library.get( library_id ) - return trans.fill_template( '/library/library.mako', library=library, group_ids=group_ids ) - - #render available libraries - libraries = [ library for library in trans.app.model.Library.select_by( deleted = False ) if renderable( library.root_folder, group_ids ) ] - return trans.fill_template( '/library/libraries.mako', group_ids=group_ids, libraries=libraries ) + def browse( self, trans, **kwd ): + return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ) ) + index = browse + @web.expose + def import_datasets( self, trans, import_ids=[], **kwd ): + if not import_ids: + return trans.show_error_message( "You must select at least one dataset to import" ) + 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 dataset(s) have been imported in to your history" % len( import_ids ), refresh_frames=['history'] ) diff --git a/static/images/expander_closed.png b/static/images/expander_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..e252606d3e68c6da135a9b165996d9da968ef7fc GIT binary patch literal 395 zcmV;60d)R}P)IO8de(|Ml<%@O-40!dwX61{2C5s-llVw2V@@N0oo_PPieZ!0Y2~+R( zk!(QTf=B;X9DnzJ@u9c>OP4(U@7{849!UlyO@H`*;lVfmCvAW6f9CF&{}ZR*{jXDW zb_vl21ozzrYJBy-Vb$aRjjJF3@7nm}zjw#A|58cE9uZ}LbIY~=6ShA8U$XeY|MDdd zfQCH!?_7WRzhvaG%|sbsT7Kz&`}!yUix%Do#>T_{_Ei`DO9UTSBkH=Hg(w4*^UnUS zTk-IJ<+2C=ZObqG7Z2FGlB7VCN;>(!bn*TFHYMl(i+Sx`L~=ArL>~EXU3lidsO!!J pWF;gqzXSh89JkLNxXeT<1_12n>%V}Y6R`jQ002ovPDHLkV1iLCz99er literal 0 HcmV?d00001 diff --git a/static/images/expander_open.png b/static/images/expander_open.png new file mode 100644 index 0000000000000000000000000000000000000000..22848b0061c86538731e46a46fbd2553c3958d45 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfvPEJt`@wP+3?}4V+dAc};Se$-3 zX}|a30Fl=7+G?sCTwr5XIC$5E)iv6chvdEs;KS86xga?O!)UU}+46=TbU4lS1SUCfIU-bU^0 z+!?^Yuw=WOS=H{VtrP1+k8bMUCGHT&;aAFjZ0cOY{>huy)(Smy%zB%rF4w)x{c&JG z{DUWpT7E}Y@2{9K=ie>W`<*7+R=nr^nrX}{_2AhpyNYva%MMjNQ5R-^c3xi9wQ+x# m+S-_||56|PL-M3Q@P*%9xbv!ydkQdc89ZJ6T-G@yGywqawXp^O literal 0 HcmV?d00001 diff --git a/static/images/folder_closed.png b/static/images/folder_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..784e8fa48234f4f64b6922a6758f254ee0ca08ec GIT binary patch literal 537 zcmV+!0_OdRP)x(K@^6+>g^d@v4;gkbWsEoXE%32*i1tcpTNXd5CcIl)ECgqz|2rE6EW}s7R?kl za1q`0GCkMruC6-2LANtwVlsgzsp4?{@7$`KBv!G66>Vie3h?3OmEEkjwdLG0PgLVi z`!N((f$A@n17Ldj#`};0I3@iHJ5M{#IZz|UIYRm4(!uV7eYIYIwQf&}_2J~}>pQ^n z6o8--^T(=hkBNQ_k{-_GWE;FMW7!p}f{NG3nHZ{D5<3d8&tLh%a4AqqnjMkr3m&fkMdECD3N5}Unig5wy40;>lo4j~k+e}v)` zR6)J8Mk*u=SpB`p6o)7j?S0T@9?bz#m@l>gc*zk__|*!FMcHwP!gwLJvS~9c0px8E zWCVGc zN?Hxg{(SJp>2>GN9JetoZ(aZH;Ije%0FdZ{S!LRVX%}YG;=d8L^N!mJkCd*SBx($jgG)S}+Le)f;q=GIn30YFNh3ZW|nh}2rL)`=3ju9K*d z<&~Gte>sT=5|RjR+}A(|O&3gS5t&*G5h0Um$c5IgEgxJl_8nzY#B+YU^|Fhvzo-^U z6fNn3(lBMcl9{Silx)4RpURdFw}1BZ?>}8S&h8fk5`hjKW@sP$LJL*otPOMf+jp$? zcC^*PiL>vkTOZln=wuc^%A^$j`TU&aa3ETVYE{(r=bgN835`stIePGw{uwD`7K9Y) z=4)VDHnkl3YL%JeLce6>Uubzae&kQ4(l6k>CGhc0FIuigU9U^L+D`6bSr zIEp(+L4eIgaZT(|{B!*DbrTYc1t0J9*MLJm+n zOEVloE20S^g6s|1rvjjuW1W$TV;&TbK2|slm=91)Q{X{Kg;c!Saglq7xo8`(>{A}G zw@`8gScq(adr(4@;>^%e<~7+uNMv&?8L0#%0yj~@DW&fPImKeyFHkRKuNERO3(SOB z?S)niZbFEN`0<|w+LjBKR#DU7F3d&rQGGY&Y)Fv0w002i=Jm_>x9jBX>wg3XCKK`E f>xbvJ_5i;DeEQXvyE?}U00000NkvXXu0mjfKqCEw literal 0 HcmV?d00001 diff --git a/static/images/library_open.png b/static/images/library_open.png new file mode 100644 index 0000000000000000000000000000000000000000..7d863f949741ff83fd8373a77c0d95a3d95e441f GIT binary patch literal 622 zcmV-!0+IcRP)YeaZ-G+53gSTz{SPWVdFiPaPX+$~@n)fi9>qgJ zh4fN-QcEhq^wI<|vImjml9)pF*W2zl+qWbTPaSyKyqWKt`DS)j3xa?yVgf%ewhl|m zA$_0x^Rx4E@d&=>uRoSN*Cm&aL#`7&zr0;L(pKMn1G#$ZszFkMC^?B8f`0w&-UD$u zd-(?iK6#!;xZ`>J^A9DGiTfguewDOKwDEKoL}`ZE_M}0=xvsgf$usmJeV$oo28d0#yu!pdgpag$giC zhlKP!CMX3wsPMGgZ5n>Xg+hV--ENofAcH61#0B7Hvl-7Il}g2A6;lfG`FxbX8A>p$ z0P;GW4il9Mr9jO9)xB^Rgy%*CSTdQ6D;kDDnM{U{5Q21FA4#ZM71$_Dba8z<-Y_Na zS@|v#c0PZNDux9AoXD-ZoWP~q)7QrC`G +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + return "" + pad = parent_pad + 20 + %> + %if parent_pad == 0: +
  • ${parent.name}
  • +
      + %else: +
    • ${parent.name}
    • +
        + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + %if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
      • ${render_dataset( dataset )}
      • + %endif + %endfor +
      + + +

      Libraries

      +
      +
        +%for library in libraries: + %if trans.app.security_agent.check_folder_contents( trans.user, library ): +
      • + + + + +
        ${library.name}FormatDbInfo
      • +
          + ${render_folder( library.root_folder, 0 )} +
        +
        + %endif +%endfor +
      + +
      diff --git a/templates/library/common.mako b/templates/library/common.mako new file mode 100644 index 00000000000..7abf13adca5 --- /dev/null +++ b/templates/library/common.mako @@ -0,0 +1,109 @@ +<%doc> + Shamelessly stolen from history... this needs to be cleaned up to remove a + bunch of stuff that doesn't apply to library datasets (like state, etc). + + +## Render the dataset `data` +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
      + %else: +
      + %endif + + ## Header row for history items (name, state, action buttons) + +
      +
      + + %if data_state == 'running': +
      + %elif data_state != 'ok': +
      + %endif +
      + <%doc> +
      + display data + edit attributes + delete +
      + + + + + + +
      ${data.display_name()}${data.ext}${data.dbkey}${data.info}
      +
      + + ## Body for history items, extra info and actions, data "peek" + +
      + %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
      + %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} +
      +
      + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
      + %if data.peek != "no peek": +
      ${data.display_peek()}
      + %endif + %else: +
      Error: unknown dataset state "${data_state}".
      + %endif + ## Recurse for child datasets + %if len( data.children ) > 0: + ## FIXME: This should not be in the template, there should + ## be a 'visible_children' method on dataset. + <% + children = [] + for child in data.children: + if child.visible: + children.append( child ) + %> + %if len( children ) > 0: +
      + There are ${len( children )} secondary datasets. + %for idx, child in enumerate(children): + ${render_dataset( child, idx + 1 )} + %endfor +
      + %endif + %endif +
      +
      + diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako deleted file mode 100644 index 1176bd2162a..00000000000 --- a/templates/library/libraries.mako +++ /dev/null @@ -1,13 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="title()">Libraries You Can Access -
      -
      Libraries You Can Access
      -
      - %for library in libraries: - - %endfor -
      -
      diff --git a/templates/library/library.mako b/templates/library/library.mako deleted file mode 100644 index 5f5440c56a0..00000000000 --- a/templates/library/library.mako +++ /dev/null @@ -1,79 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="render_component( component )"> - <% - if isinstance( component, trans.app.model.LibraryFolder ): - render = False - # Check the folder's datasets to see what can be rendered - for library_folder_dataset_assoc in component.datasets: - if render: - break - dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - render = True - break - # Check the folder's sub-folders to see what can be rendered - for library_folder in component.folders: - render_component( library_folder ) - if render: - return render_folder( component ) - elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - render = False - dataset = trans.app.model.Dataset.get( component.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - render = True - break - if render: - return render_dataset( component ) - %> - - -## Render the dataset `data` as history item, using `hid` as the displayed id -<%def name="render_dataset( data )"> -
      - ${data.name} - view or edit attributes -
      - - -## Render a folder -<%def name="render_folder( this_folder )"> -
      - Folder: ${this_folder.name} - <% - components = this_folder.active_components - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> -
      - %for component in components: - ${render_component( component )} - %endfor -
      -
      - - -<%def name="title()">View Library: ${library.name} -
      -
      Import from Library: ${library.name}
      -
      -
      - ${render_folder( library.root_folder )} -
      - -
      -
      -
      diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 2336f5d43c7..e91445921d8 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -6,7 +6,11 @@ else: data_state = data.state %> -
      + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
      + %else: +
      + %endif ## Header row for history items (name, state, action buttons) diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index 69ccfe583ee..03ac818620b 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -2,6 +2,7 @@
      + @@ -12,7 +13,6 @@ -
      diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml index e6dabfbf1b1..1281386f7ce 100644 --- a/tools/data_source/access_libraries.xml +++ b/tools/data_source/access_libraries.xml @@ -1,7 +1,7 @@ stored locally - + - \ No newline at end of file + From 9030c528b0b90c7dafebcc8d79517082b16e3c81 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 10 Sep 2008 11:21:57 -0400 Subject: [PATCH 50/94] imported patch workflow-index-bug --- lib/galaxy/web/controllers/workflow.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 502b9cdc8e7..ac1a7f10ab6 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -67,7 +67,7 @@ class WorkflowController( BaseController ): session.save( share ) session.flush() trans.set_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) ) - return self.index( trans ) + return self.list( trans ) return trans.fill_template( "workflow/share.mako", message = msg, messagetype = mtype, @@ -82,7 +82,7 @@ class WorkflowController( BaseController ): stored.name = new_name trans.sa_session.flush() trans.set_message( "Workflow renamed to '%s'." % new_name ) - return self.index( trans ) + return self.list( trans ) else: return form( url_for( id=trans.security.encode_id(stored.id) ), "Rename workflow", submit_text="Rename" ) \ .add_text( "new_name", "Workflow Name", value=stored.name ) @@ -111,7 +111,7 @@ class WorkflowController( BaseController ): session.flush() # Display the management page trans.set_message( 'Clone created with name "%s"' % new_stored.name ) - return self.index( trans ) + return self.list( trans ) @web.expose @web.require_login( "create workflows" ) @@ -136,7 +136,7 @@ class WorkflowController( BaseController ): session.flush() # Display the management page trans.set_message( "Workflow '%s' created" % stored_workflow.name ) - return self.index( trans ) + return self.list( trans ) else: return form( url_for(), "Create new workflow", submit_text="Create" ) \ .add_text( "workflow_name", "Workflow Name", value="Unnamed workflow" ) @@ -153,7 +153,7 @@ class WorkflowController( BaseController ): stored.flush() # Display the management page trans.set_message( "Workflow '%s' deleted" % stored.name ) - return self.index( trans ) + return self.list( trans ) @web.expose @web.require_login( "edit workflows" ) @@ -437,8 +437,7 @@ class WorkflowController( BaseController ): trans.sa_session.save( stored ) trans.sa_session.flush() # Index page with message - trans.template_context['message'] = "Workflow '%s' created" % workflow_name - return self.index( trans ) + return trans.show_message( "Workflow '%s' created from current history." % workflow_name ) ## return trans.show_ok_message( "

      Workflow '%s' created.

      Click to load in workflow editor

      " ## % ( workflow_name, web.url_for( action='editor', id=trans.security.encode_id(stored.id) ) ) ) From b1caa7431aed31b601497e2bb0eacd98fe0e4821 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 10 Sep 2008 23:22:06 -0400 Subject: [PATCH 51/94] Admin side of the new file browser. It's late, I've been working all night, and I'm not sure what my name is... so there are probably bugs. Please play with this stuff and try to break it. This commit also includes permissions editing that actually (finally!) lets you create new group->dataset associations after the dataset already exists. Currently the only entry point is from the Library, but the "display a group's datasets" page can now be extended to contain useful information about associated datasets (like whose history a dataset is part of and what folder a dataset is in) to make it actually, like, useful and stuff. --- lib/galaxy/security/__init__.py | 2 +- lib/galaxy/web/controllers/admin.py | 377 ++++++++++-------- .../{library_closed.png => silk/book.png} | Bin .../{library_open.png => silk/book_open.png} | Bin .../{folder_closed.png => silk/folder.png} | Bin .../{folder_open.png => silk/folder_page.png} | Bin .../resultset_bottom.png} | Bin .../resultset_next.png} | Bin templates/admin/library/browser.mako | 162 ++++++++ .../admin/library/change_permissions.mako | 57 --- templates/admin/library/common.mako | 253 ++++++++++++ templates/admin/library/dataset.mako | 35 +- templates/admin/library/folder.mako | 170 -------- templates/admin/library/libraries.mako | 34 -- templates/admin/library/library.mako | 38 -- templates/admin/library/new_dataset.mako | 11 +- templates/library/browser.mako | 18 +- 17 files changed, 651 insertions(+), 506 deletions(-) rename static/images/{library_closed.png => silk/book.png} (100%) rename static/images/{library_open.png => silk/book_open.png} (100%) rename static/images/{folder_closed.png => silk/folder.png} (100%) rename static/images/{folder_open.png => silk/folder_page.png} (100%) rename static/images/{expander_open.png => silk/resultset_bottom.png} (100%) rename static/images/{expander_closed.png => silk/resultset_next.png} (100%) create mode 100644 templates/admin/library/browser.mako delete mode 100644 templates/admin/library/change_permissions.mako create mode 100644 templates/admin/library/common.mako delete mode 100644 templates/admin/library/folder.mako delete mode 100644 templates/admin/library/libraries.mako delete mode 100644 templates/admin/library/library.mako diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 6782bbe3089..4bd8cf2fe44 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -209,7 +209,7 @@ class GalaxyRBACAgent( RBACAgent ): for data in history.datasets: for hda in data.dataset.history_associations: if history.user and hda.history not in history.user.histories: - # When would this occur? + # This will occur when a user logs in and has datasets in their previously-public history. self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) break else: diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f6608cfc879..08673f53ed6 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -542,74 +542,119 @@ class Admin( BaseController ): # Galaxy Library Stuff @web.expose - def libraries( self, trans, **kwd ): + def library_browser( self, trans, **kwd ): + ## TODO: "show deleted libraries" toggle? 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/library/libraries.mako', libraries=trans.app.model.Library.select_by( deleted = False ), msg=msg ) - @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 ) - params = util.Params( kwd ) - msg = params.msg - if 'create_library' in kwd: - if len( trans.app.model.Library.select_by( name=name ) ) > 0: - msg = "A library with that name already exists" - trans.response.send_redirect( web.url_for( action='libraries', msg=msg ) ) - 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, msg=msg ) ) - 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, msg=msg ) + if 'message' in kwd: + message = kwd['message'] else: - return trans.show_error_message( "Invalid library specified" ) + message = None + return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ), message = message ) + libraries = library_browser @web.expose - def folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): + def library( self, trans, id=None, **kwd ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) + if not id and 'new' not in kwd: + return trans.show_error_message( "Galaxy can't perform a library action if you don't specify a library" ) + if 'new' not in kwd: + library = trans.app.model.Library.get( id ) params = util.Params( kwd ) - msg = params.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, msg=msg ) ) - elif id is None: + if 'new' in kwd: + if params.new == 'submitted': + library = trans.app.model.Library( name = params.name, description = params.description ) + root_folder = trans.app.model.LibraryFolder( name = params.name, description = "" ) + root_folder.flush() + library.root_folder = root_folder + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) 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 + web.FormBuilder( action = web.url_for(), title = "Create a new Library", name="library", submit_text = "Create" ) + .add_text( name = "name", label = "Name", value = "New Library" ) + .add_text( name = "description", label = "Description", value = "" ) + .add_input( 'hidden', '', 'new', 'submitted', use_label = False ) ) + elif 'rename' in kwd: + if params.rename == 'submitted': + if 'root_folder' in kwd: + root_folder = library.root_folder + root_folder.name = params.name + root_folder.flush() + library.name = params.name + library.description = params.description + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Edit library name and description", name = "library", submit_text = "Save" ) + .add_text( name = "name", label = "Name", value = library.name ) + .add_text( name = "description", label = "Description", value = library.description ) + .add_input( 'checkbox', 'Also change the root folder\'s name', 'root_folder' ) + .add_input( 'hidden', '', 'rename', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'delete' in kwd: + def delete_folder( folder ): + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for dataset in folder.active_datasets: + dataset.deleted = True + dataset.flush() + folder.deleted = True folder.flush() - msg = 'Folder has been renamed.' - return trans.fill_template( '/admin/library/folder.mako', folder=folder, msg=msg ) + delete_folder( library.root_folder ) + library.deleted = True + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) else: - return trans.show_error_message( "Invalid folder specified" ) + return trans.show_error_message( "Galaxy can't perform a library action if you don't specify an action" ) + @web.expose + def folder( self, trans, id=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if not id: + return trans.show_error_message( "Galaxy can't perform a folder action if you don't specify a folder" ) + params = util.Params( kwd ) + folder = trans.app.model.LibraryFolder.get( id ) + if 'new' in kwd: + if params.new == 'submitted': + new_folder = trans.app.model.LibraryFolder( name = params.name, description = params.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 (?) + new_folder.genome_build = util.dbnames.default_value + folder.add_folder( new_folder ) + new_folder.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new folder", name="folder", submit_text = "Create" ) + .add_text( name = "name", label = "Name", value = "New Folder" ) + .add_text( name = "description", label = "Description", value = "" ) + .add_input( 'hidden', '', 'new', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'rename' in kwd: + if params.rename == 'submitted': + folder.name = params.name + folder.description = params.description + folder.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Edit folder name and description", name = "folder", submit_text = "Save" ) + .add_text( name = "name", label = "Name", value = folder.name ) + .add_text( name = "description", label = "Description", value = folder.description ) + .add_input( 'hidden', '', 'rename', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'delete' in kwd: + def delete_folder( folder ): + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for dataset in folder.active_datasets: + dataset.deleted = True + dataset.flush() + folder.deleted = True + folder.flush() + delete_folder( folder ) + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + else: + return trans.show_error_message( "Galaxy can't perform a folder action if you don't specify an action" ) @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 ): @@ -625,12 +670,6 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg - def listify( item ): - if isinstance( item, list ): - return item - else: - return [ item ] - # 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 @@ -703,6 +742,7 @@ class Admin( BaseController ): return dataset # END add_file method + # Dataset upload if 'create_dataset' in kwd: # Copied from upload tool action last_dataset_created = None @@ -794,13 +834,25 @@ class Admin( BaseController ): info="imported file", space_to_tab=space_to_tab ) created_datasets.append( last_dataset_created ) - if len( created_datasets ): - trans.response.send_redirect( web.url_for( action='change_permissions', ids=",".join( [ str(d.id) for d in created_datasets ] ) ) ) + if len( created_datasets ) > 1: + trans.response.send_redirect( web.url_for( + action = 'library_browser', + message = "%i new datasets added to the library. Click here if you'd like to edit the permissions on these datasets." % ( + len( created_datasets ), + web.url_for( action='dataset', id=",".join( [ str(d.id) for d in created_datasets ] ) ) + ) + ) ) elif last_dataset_created is not None: - trans.response.send_redirect( web.url_for( action='dataset', id=last_dataset_created.id ) ) + trans.response.send_redirect( web.url_for( + action = 'library_browser', + message = "New dataset added to the library. Click here if you'd like to edit the permissions or attributes on this dataset." % + web.url_for( action='dataset', id=last_dataset_created.id ) + ) ) else: return trans.show_error_message( 'Upload failed' ) - elif id is None: + + # No dataset(s) specified, display upload form + elif not id: # 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 @@ -823,45 +875,21 @@ class Admin( BaseController ): last_used_build=last_used_build, groups=groups, msg=msg ) - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) - if dataset: + else: + if id.count( ',' ): + ids = id.split(',') + id = None + else: + ids = None + # id specified, display attributes form + if id: + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if not dataset: + return trans.show_error_message( "Invalid dataset specified" ) + # 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 'Dataset Permissions' 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: + if 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() @@ -879,7 +907,7 @@ class Admin( BaseController ): 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" ) @@ -894,24 +922,11 @@ class Admin( BaseController ): dataset.datatype.after_edit( dataset ) trans.app.model.flush() return trans.show_ok_message( "Attributes updated" ) - + elif p.delete: + dataset.deleted = True + dataset.flush() + trans.response.send_redirect( web.url_for( action='library_browser' ) ) 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 @@ -931,12 +946,28 @@ class Admin( BaseController ): dataset=dataset, metadata=metadata, datatypes=ldatatypes, - dataset_actions=dataset_actions, - gdas=gdas, err=None, msg=msg ) - else: - return trans.show_error_message( "Invalid dataset specified" ) + # multiple ids specfied, display multi permission form + elif ids: + datasets = [] + for id in [ int( id ) for id in ids ]: + d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if d is None: + return trans.show_error_message( 'You specified an invalid dataset' ) + datasets.append( d ) + if len( datasets ) < 2: + return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) + # If the permissions on the first dataset don't match the intersection + # of permissions, the permissions across all datasets are not + # identical. Although, should we care, or should we just let the admin + # overwrite permissions regardless? + if trans.app.security_agent.get_dataset_permissions( datasets[0] ) != \ + trans.app.security_agent.guess_derived_permissions_for_datasets( [ d.dataset for d in datasets ] ): + return trans.show_error_message( "The datasets you selected do not have identical permissions, so they can not be updated together" ) + else: + return trans.fill_template( "/admin/library/dataset.mako", + dataset=datasets ) def check_gzip( self, temp_name ): """ Utility method to check gzipped uploads @@ -954,42 +985,59 @@ class Admin( BaseController ): # return( True, False ) return ( True, True ) @web.expose - def change_permissions( self, trans, ids=[], **kwd ): + def dataset_permissions( self, trans, id=None, **kwd ): + ''' + In this method, id is an actual Dataset object, not an association. + ''' if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) - if not ids: - return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) - datasets = [] - for id in [ int( id ) for id in ids.split(',') ]: - d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) - if d is None: - return trans.show_error_message( 'You specified an invalid dataset' ) - datasets.append( d ) - if len( datasets ) == 0: - return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) - elif len( datasets ) == 1: - trans.response.send_redirect( web.url_for( action='dataset', id=datasets[0].id ) ) - # If the permissions on the first dataset don't match the intersection - # of permissions, the permissions across all datasets are not - # identical. Although, should we care, or should we just let the admin - # overwrite permissions regardless? - if trans.app.security_agent.get_dataset_permissions( datasets[0] ) != \ - trans.app.security_agent.guess_derived_permissions_for_datasets( [ d.dataset for d in datasets ] ): - return trans.show_error_message( "The datasets you selected do not have identical permissions, so they can not be updated together" ) - if 'change_permissions' in kwd: - group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] - group_ids_checked = filter( lambda x: not x.count('_'), group_args ) - permissions = [] - for group_id in group_ids_checked: - action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] - actions = trans.app.security_agent.convert_permitted_action_strings( action_strings ) - permissions.append( ( trans.app.security_agent.get_group( group_id ), actions ) ) - for d in datasets: - trans.app.security_agent.set_dataset_permissions( d.dataset, permissions ) - return trans.show_ok_message( "Dataset permissions have been set." ) + if not id: + return trans.show_error_message( 'You must specify at least one dataset to modify permissions on' ) + params = util.Params( kwd ) + # id can be a list of comma separated datasets, too. + if id.count( ',' ): + ids = id.split( ',' ) else: - return trans.fill_template( "/admin/library/change_permissions.mako", - datasets=datasets ) + ids = [ id ] + datasets = [] + for d_id in ids: + d = trans.app.model.Dataset.get( d_id ) + if not d: + return trans.show_error_message( 'You specified an invalid dataset' ) + datasets.append( d ) + if 'change_permitted_actions' in kwd: + users = [] + groups = [] + if params.users: + users = listify( params.users ) + if params.groups: + groups = listify( params.groups ) + permissions = [] + for group_id in users + groups: + permitted_actions = [ pa.replace( group_id + ',', '' ) for pa in params.actions if pa.startswith( group_id + ',' ) ] + permitted_actions = trans.app.security_agent.convert_permitted_action_strings( permitted_actions ) + permissions.append( ( trans.app.model.Group.get( int( group_id ) ), permitted_actions ) ) + if params.public: + permissions.append( ( trans.app.security_agent.get_public_group(), trans.app.security_agent.permitted_actions.DATASET_ACCESS ) ) + for dataset in datasets: + trans.app.security_agent.set_dataset_permissions( dataset, permissions ) + elif 'create_group_associations' in kwd: + users = [] + groups = [] + if params.users: + users = listify( params.users ) + if params.groups: + groups = listify( params.groups ) + if params.public: + for dataset in datasets: + trans.app.security_agent.associate_components( group=trans.app.security_agent.get_public_group(), dataset=dataset ) + for group_id in users + groups: + for dataset in datasets: + trans.app.security_agent.associate_components( group=trans.app.model.Group.get( int( group_id ) ), dataset=dataset ) + if params.lid: + trans.response.send_redirect( web.url_for( action='dataset', id=params.lid ) ) + else: + trans.response.send_redirect( web.url_for( action='library_browser' ) ) @web.expose def delete_dataset( self, trans, id=None, **kwd): if not self.user_is_admin( trans ): @@ -1072,3 +1120,12 @@ class Admin( BaseController ): trans.log_event( "Library id %s deleted." % id ) trans.response.send_redirect( web.url_for( action = 'libraries', msg = 'You have deleted the library %s.' % library.id ) ) return trans.show_error_message( "You did not specify a library to delete." ) + +def listify( item ): + """ + Since single params are not a single item list + """ + if isinstance( item, list ): + return item + else: + return [ item ] diff --git a/static/images/library_closed.png b/static/images/silk/book.png similarity index 100% rename from static/images/library_closed.png rename to static/images/silk/book.png diff --git a/static/images/library_open.png b/static/images/silk/book_open.png similarity index 100% rename from static/images/library_open.png rename to static/images/silk/book_open.png diff --git a/static/images/folder_closed.png b/static/images/silk/folder.png similarity index 100% rename from static/images/folder_closed.png rename to static/images/silk/folder.png diff --git a/static/images/folder_open.png b/static/images/silk/folder_page.png similarity index 100% rename from static/images/folder_open.png rename to static/images/silk/folder_page.png diff --git a/static/images/expander_open.png b/static/images/silk/resultset_bottom.png similarity index 100% rename from static/images/expander_open.png rename to static/images/silk/resultset_bottom.png diff --git a/static/images/expander_closed.png b/static/images/silk/resultset_next.png similarity index 100% rename from static/images/expander_closed.png rename to static/images/silk/resultset_next.png diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako new file mode 100644 index 00000000000..e520ac4723f --- /dev/null +++ b/templates/admin/library/browser.mako @@ -0,0 +1,162 @@ +<%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + ##if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + ## return "" + pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True + %> +
    • +
      + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif + +
      + +
    • + %if subfolder: +
        + %else: +
          + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + ##%if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
        • ${render_dataset( dataset )}
        • + ##%endif + %endfor +
        + + +

        Libraries

        + +%if message: +<% + try: + messagetype + except: + messagetype = "done" +%> +

        +

        + ${message} +
        +

        +%endif + +

        +
        +
          +%for library in libraries: + ##%if trans.app.security_agent.check_folder_contents( trans.user, library ): +
        • + + + + +
          + + ${library.name} + %if library.description: + - ${library.description} + %endif + + + FormatDbInfo
        • +
            + ${render_folder( library.root_folder, 0 )} +
          +
          + ##%endif +%endfor +
        +## +
        diff --git a/templates/admin/library/change_permissions.mako b/templates/admin/library/change_permissions.mako deleted file mode 100644 index a3be1b647d0..00000000000 --- a/templates/admin/library/change_permissions.mako +++ /dev/null @@ -1,57 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Change Dataset Permissions - - -
        -
        Change Dataset Access Permissions
        -
        -
        - -
        - <% groups = trans.app.model.Group.select() %> - <% active_group_ids = [ assoc.group.id for assoc in datasets[0].dataset.groups ] %> -
        - Check each group which should have access to this dataset. -
        - %for group in groups: - %if group.id in active_group_ids: - <% assoc = filter( lambda x: x.group_id == group.id, datasets[0].dataset.groups )[0] %> - %else: - <% assoc = None %> - %endif - ${group.name}
        -
        - %for k, v in trans.app.security_agent.permitted_actions.items(): - ${trans.app.security_agent.get_permitted_action_description(k)}
        - %endfor -
        - %endfor - -
        -
        -
        -
        diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako new file mode 100644 index 00000000000..cdaf5492f90 --- /dev/null +++ b/templates/admin/library/common.mako @@ -0,0 +1,253 @@ +<%def name="render_permissions_forms( data_obj )"> + + +<% + redirect = None + id = None + if isinstance( data_obj, trans.app.model.LibraryFolderDatasetAssociation ): + dataset = data_obj.dataset + redirect = ("lid", data_obj.id) + elif isinstance( data_obj, list ): + dataset = data_obj[0].dataset + id = ",".join( [ str(d.dataset.id) for d in data_obj ] ) + if isinstance( data_obj[0], trans.app.model.LibraryFolderDatasetAssociation ): + redirect = ("lid", ",".join( [ str(d.id) for d in data_obj ] ) ) + else: + trans.show_error_message( "Unknown object passed to render_permissions_forms" ) + if id is None: + id = dataset.id +%> + +
        +
        Change Existing Permissions
        +
        +
        + + %if redirect: + + %endif +
        + <% + user_permissions = [ p for p in trans.app.security_agent.get_dataset_permissions( dataset ) if p[0].name.endswith( ' private group' ) ] + real_permissions = [ p for p in trans.app.security_agent.get_dataset_permissions( dataset ) if not p[0].name.endswith( ' private group' ) and p[0].name != 'public' ] + %> +
        + Update the permissions for the users and groups currently associated with this dataset. +
        +
        + %if trans.app.security_agent.dataset_has_group( dataset.id, trans.app.security_agent.get_public_group().id ): +
        + This dataset can be accessed by anyone (it is public).
        + %endif +
        +
        + %if not len( user_permissions ): +   None
        + %endif + %for p in user_permissions: + ${p[0].name.replace( ' private group', '' )}
        +
        + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
        + %endfor +
        + %endfor +
        +
        + %if not len( real_permissions ): +   None
        + %endif + %for p in real_permissions: + ${p[0].name}
        +
        + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
        + %endfor +
        + %endfor +
        +
        +
        +
        +
        +
        +

        + +

        +
        Add New Users and Groups
        +
        +
        + + %if redirect: + + %endif +
        +
        + Select new users and groups to associate with this dataset (you can assign permissions after creating the association). +
        + ## Don't display the public access checkbox if it's already public (it's shown above instead) + %if not trans.app.security_agent.dataset_has_group( dataset.id, trans.app.security_agent.get_public_group().id ): +
        + This dataset can be accessed by anyone (make it public).
        + %endif +
        + <% + all_groups = trans.app.model.Group.select() + user_groups = [ g for g in all_groups if g.name.endswith( ' private group' ) ] + real_groups = [ g for g in all_groups if not g.name.endswith( ' private group' ) and g.name != 'public' ] + %> +
        + + +

        + + +

        +
        + To select multiple users or groups, hold ctrl or command while clicking. +
        +
        +
        +
        +
        +
        +

        + + + +<%doc> + Shamelessly stolen from history... this needs to be cleaned up to remove a + bunch of stuff that doesn't apply to library datasets (like state, etc). + + +## Render the dataset `data` +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +

        + %else: +
        + %endif + + ## Header row for history items (name, state, action buttons) + +
        +
        + + %if data_state == 'running': +
        + %elif data_state != 'ok': +
        + %endif +
        + <%doc> +
        + display data + edit attributes + delete +
        + + + + + + +
        + + ${data.display_name()} + + + ${data.ext}${data.dbkey}${data.info}
        +
        + + ## Body for history items, extra info and actions, data "peek" + +
        +
        + ${data.blurb} +
        +
        + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
        + %if data.peek != "no peek": +
        ${data.display_peek()}
        + %endif + ## Recurse for child datasets + %if len( data.children ) > 0: + ## FIXME: This should not be in the template, there should + ## be a 'visible_children' method on dataset. + <% + children = [] + for child in data.children: + if child.visible: + children.append( child ) + %> + %if len( children ) > 0: +
        + There are ${len( children )} secondary datasets. + %for idx, child in enumerate(children): + ${render_dataset( child, idx + 1 )} + %endfor +
        + %endif + %endif +
        +
        + diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index ea17090632f..adb15e2cd4f 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -1,4 +1,5 @@ <%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_permissions_forms" /> <%def name="title()">Edit Dataset Attributes @@ -15,34 +16,9 @@ -
        -
        Dataset Permissions
        -
        -
        - -
        - <% dataset_gdas = [ assoc for assoc in dataset.dataset.groups ] %> -
        - Choose the permissions each user or group should have on this dataset. -
        - %for gda in dataset_gdas: - ${gda.group.name.replace( ' private group', '' )}
        -
        - %for k, v in trans.app.security_agent.permitted_actions.items(): - ${trans.app.security_agent.get_permitted_action_description(k)}
        - %endfor -
        - %endfor -
        -
        -
        -
        -
        -

        +${render_permissions_forms( dataset )} + +%if not isinstance( dataset, list ):

        Edit Attributes
        @@ -111,5 +87,6 @@
        -manage containing folder

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

        -
        ${data.display_name()}
        -
        -
        - ## Header row for folder items (name, state, action buttons) -
        - %if data_state != 'ok': -
        - %endif -
        -
        - edit attributes - -
        - ##${data.display_name()} -
        - ## Body for folder items, extra info and actions, data "peek" -
        - %if data_state == "queued": -
        Job is waiting to run
        - %elif data_state == "running": -
        Job is currently running
        - %elif data_state == "error": -
        - An error occurred running this job: ${data.display_info().strip()}, - report this error -
        - %elif data_state == "empty": -
        No data: ${data.display_info()}
        - %elif data_state == "ok": -
        - ${data.blurb}, - format: ${data.ext}, - database: - %if data.dbkey == '?': - ${data.dbkey} - %else: - ${data.dbkey} - %endif -
        -
        Info: ${data.display_info()}
        - %if data.peek != "no peek": -
        ${data.display_peek()}
        - %endif - %else: -
        Error: unknown dataset state "${data_state}".
        - %endif - ## Recurse for child datasets -
        -
        -
        - - -## Render a folder -<%def name="render_folder( this_folder )"> -
        -
        Contents of Folder: ${this_folder.name}
        -
        -
        - <% - components = this_folder.active_components - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> - %for component in components: - ${render_component( component )} - %endfor -
        -
        - -
        -
        -
        - -<%def name="title()">Manage Folder: ${folder.name} -
        -
        - Libraries  |   - Groups  |   - Users -
        -
        Change Folder Attributes
        -
        -
        -
        - -
        - -
        -
        -
        -
        - -
        - -
        -
        -
        -
        -
        - -
        -
        - -
        -
        - -
        -
        -
        -
        -
        -
        Manage Folder Contents: ${folder.name}
        -
        -
        - %if folder.parent: - Up a Level | - %elif folder.library_root: - Manage Library | - %endif - delete folder -
        -
        -
        - ${render_folder( folder )} -
        -
        -
        -
        diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako deleted file mode 100644 index 3511b32a923..00000000000 --- a/templates/admin/library/libraries.mako +++ /dev/null @@ -1,34 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="title()">Libraries - -%if msg: -
        ${msg}
        -%endif - -

        Libraries

        - - - -%if len(libraries) == 0: - - There are no libraries - -%else: - - - - - - - %for library in libraries: - - - - - %endfor -
        NameDescription
        ${library.name}${library.description}
        - -%endif \ No newline at end of file diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako deleted file mode 100644 index 781a8b584ae..00000000000 --- a/templates/admin/library/library.mako +++ /dev/null @@ -1,38 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="title()">Library - -%if msg: -
        ${msg}
        -%endif - -
        -
        Library '${library.name}'
        -
        -
        - -
        - -
        - -
        -
        -
        -
        - -
        - -
        -
        -
        - - -
         
        -
        -
        -
        -
        - diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 1154e3af8bf..0876d1a2c02 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -3,15 +3,10 @@ <% import os %> <%def name="title()">Create New Library Dataset +%if msg: +

        ${msg}

        +%endif
        -
        - Libraries  |   - Groups  |   - Users -
        - %if msg: -

        ${msg}

        - %endif
        Create a new Library Dataset
        diff --git a/templates/library/browser.mako b/templates/library/browser.mako index 5992519e3b2..5a8627ea3bb 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -16,19 +16,19 @@ q("li.libraryOrFolderRow").wrap( "" ).click( function() { var contents = q(this).parent().next("ul"); if ( this.id == "libraryRow" ) { - var icon_open = "${h.url_for( '/static/images/library_open.png' )}"; - var icon_closed = "${h.url_for( '/static/images/library_closed.png' )}"; + var icon_open = "${h.url_for( '/static/images/silk/book_open.png' )}"; + var icon_closed = "${h.url_for( '/static/images/silk/book.png' )}"; } else { - var icon_open = "${h.url_for( '/static/images/folder_open.png' )}"; - var icon_closed = "${h.url_for( '/static/images/folder_closed.png' )}"; + var icon_open = "${h.url_for( '/static/images/silk/folder_page.png' )}"; + var icon_closed = "${h.url_for( '/static/images/silk/folder.png' )}"; } if ( contents.is(":visible") ) { contents.slideUp("fast"); - q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/expander_closed.png' )}"; }); + q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/silk/resultset_next.png' )}"; }); q(this).children().find("img.rowIcon").each( function() { this.src = icon_closed; }); } else { contents.slideDown("fast"); - q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/expander_open.png' )}"; }); + q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/silk/resultset_bottom.png' )}"; }); q(this).children().find("img.rowIcon").each( function() { this.src = icon_open; }); } }); @@ -65,10 +65,10 @@ pad = parent_pad + 20 %> %if parent_pad == 0: -
      • ${parent.name}
      • +
      • ${parent.name}
        • %else: -
        • ${parent.name}
        • +
        • ${parent.name}
          • %endif %for folder in parent.active_folders: @@ -88,7 +88,7 @@ %for library in libraries: %if trans.app.security_agent.check_folder_contents( trans.user, library ):
          • - + From e99e6164e2ca3ecc2592c12a9b6fabc3d5b82c92 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 12 Sep 2008 11:00:47 -0400 Subject: [PATCH 52/94] Fix link colors in admin. --- templates/admin/index.mako | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/admin/index.mako b/templates/admin/index.mako index 04661b69412..e3d642f55b3 100644 --- a/templates/admin/index.mako +++ b/templates/admin/index.mako @@ -57,6 +57,11 @@ display: list-item; list-style: square outside; } + a:link, a:visited, a:active + { + color: #303030; + } + @@ -104,4 +109,4 @@ - \ No newline at end of file + From f2f7550188bb45ca463172c5fe9d456210ba1474 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 12 Sep 2008 14:54:06 -0400 Subject: [PATCH 53/94] A couple bugfixes and a multi-update for datasets in the admin-side browser. --- lib/galaxy/web/controllers/admin.py | 38 +++++++++++++++----- templates/admin/library/browser.mako | 21 +++++++++-- templates/admin/library/common.mako | 2 +- templates/library/browser.mako | 52 ++++++++++++++++++++++++---- templates/library/common.mako | 28 +++++++-------- 5 files changed, 109 insertions(+), 32 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 08673f53ed6..958e6234663 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -563,8 +563,8 @@ class Admin( BaseController ): params = util.Params( kwd ) if 'new' in kwd: if params.new == 'submitted': - library = trans.app.model.Library( name = params.name, description = params.description ) - root_folder = trans.app.model.LibraryFolder( name = params.name, description = "" ) + library = trans.app.model.Library( name = util.restore_text( params.name ), description = util.restore_text( params.description ) ) + root_folder = trans.app.model.LibraryFolder( name = util.restore_text( params.name ), description = "" ) root_folder.flush() library.root_folder = root_folder library.flush() @@ -578,10 +578,10 @@ class Admin( BaseController ): if params.rename == 'submitted': if 'root_folder' in kwd: root_folder = library.root_folder - root_folder.name = params.name + root_folder.name = util.restore_text( params.name ) root_folder.flush() - library.name = params.name - library.description = params.description + library.name = util.restore_text( params.name ) + library.description = util.restore_text( params.description ) library.flush() return trans.response.send_redirect( web.url_for( action='library_browser' ) ) return trans.show_form( @@ -616,7 +616,7 @@ class Admin( BaseController ): folder = trans.app.model.LibraryFolder.get( id ) if 'new' in kwd: if params.new == 'submitted': - new_folder = trans.app.model.LibraryFolder( name = params.name, description = params.description ) + new_folder = trans.app.model.LibraryFolder( name = util.restore_text( params.name ), description = util.restore_text( params.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 (?) @@ -632,8 +632,8 @@ class Admin( BaseController ): .add_input( 'hidden', '', 'id', id, use_label = False ) ) elif 'rename' in kwd: if params.rename == 'submitted': - folder.name = params.name - folder.description = params.description + folder.name = util.restore_text( params.name ) + folder.description = util.restore_text( params.description ) folder.flush() return trans.response.send_redirect( web.url_for( action='library_browser' ) ) return trans.show_form( @@ -908,6 +908,7 @@ class Admin( BaseController ): else: setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) + dataset.metadata.dbkey = dbkey dataset.datatype.after_edit( dataset ) trans.app.model.flush() return trans.show_ok_message( "Attributes updated" ) @@ -1039,6 +1040,27 @@ class Admin( BaseController ): else: trans.response.send_redirect( web.url_for( action='library_browser' ) ) @web.expose + def datasets( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + if 'with-selected' in kwd: + if not params.dataset_ids: + return trans.show_error_message( "At least one dataset must be selected." ) + dataset_ids = listify( params.dataset_ids ) + if params.action == 'edit': + trans.response.send_redirect( web.url_for( action = 'dataset', id = ",".join( dataset_ids ) ) ) + elif params.action == 'delete': + for id in dataset_ids: + d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + d.deleted = True + d.flush() + trans.response.send_redirect( web.url_for( action = 'library_browser' ) ) + else: + return trans.show_error_message( "Not implemented." ) + else: + return trans.show_error_message( "Galaxy can't operate on datasets without an operation." ) + @web.expose def delete_dataset( self, trans, id=None, **kwd): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako index e520ac4723f..31037536dbe 100644 --- a/templates/admin/library/browser.mako +++ b/templates/admin/library/browser.mako @@ -56,6 +56,15 @@ }); }); }); + function checkForm() { + if ( $("select#with-selected-select option:selected").text() == "delete" ) { + if ( confirm( "Are you sure you want to delete these datasets?" ) ) { + return true; + } else { + return false; + } + } + } <%def name="render_folder( parent, parent_pad )"> @@ -130,7 +139,7 @@ - +
              %for library in libraries: ##%if trans.app.security_agent.check_folder_contents( trans.user, library ): @@ -158,5 +167,13 @@ ##%endif %endfor
            -## +
            + With selected datasets: + + +
            diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako index cdaf5492f90..7e3423e0786 100644 --- a/templates/admin/library/common.mako +++ b/templates/admin/library/common.mako @@ -192,7 +192,7 @@
            ${library.name} ${library.name} Format Db Info
            - + ${data.display_name()}
            diff --git a/templates/library/browser.mako b/templates/library/browser.mako index 5a8627ea3bb..ef0f94a5cfe 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -58,18 +58,52 @@ }); + + + + <%def name="render_folder( parent, parent_pad )"> <% if not trans.app.security_agent.check_folder_contents( trans.user, parent ): return "" pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True %> - %if parent_pad == 0: -
          • ${parent.name}
          • -
              - %else: -
            • ${parent.name}
            • +
            • +
              + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif +
              +
            • + %if subfolder:
                + %else: +
                  %endif %for folder in parent.active_folders: ${render_folder( folder, pad )} @@ -88,7 +122,13 @@ %for library in libraries: %if trans.app.security_agent.check_folder_contents( trans.user, library ):
                • - + diff --git a/templates/library/common.mako b/templates/library/common.mako index 7abf13adca5..9861833113e 100644 --- a/templates/library/common.mako +++ b/templates/library/common.mako @@ -38,7 +38,18 @@
                  ${library.name} + + ${library.name} + %if library.description: + - ${library.description} + %endif + Format Db Info
                  - + @@ -50,18 +61,7 @@
                  %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
                  - %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": + %else:
                  ${data.blurb}
                  @@ -82,8 +82,6 @@ %if data.peek != "no peek":
                  ${data.display_peek()}
                  %endif - %else: -
                  Error: unknown dataset state "${data_state}".
                  %endif ## Recurse for child datasets %if len( data.children ) > 0: From 4e87b28149a0a97cd54c3985767ae54fc27d5404 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 16 Sep 2008 13:53:07 -0400 Subject: [PATCH 54/94] Make the user aware of permissions issues when they share a history. --- lib/galaxy/web/controllers/root.py | 34 ++++++++++++ templates/history/share.mako | 84 +++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index aff793e79cf..2dab4c970bc 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -450,11 +450,45 @@ class RootController( BaseController ): return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) user = trans.get_user() send_to_user = trans.app.model.User.get_by( email = email ) + p = util.Params( kwd ) + if p.action: + if p.action == "no_share": + trans.response.send_redirect( url_for( action='history_options' ) ) + try: + send_to_group = trans.app.model.Group.select_by( name = send_to_user.email + ' private group' )[0] + except: + send_to_group = None + if not send_to_group: + return trans.show_error_message( "Couldn't locate %s's private group, please report this error." % user.email ) if not send_to_user: send_to_err = "No such user" elif user.email == email: send_to_err = "You can't send histories to yourself" else: + # if we're not checking or changing permissions, skip this step + if not p.action or ( p.action and p.action != 'share' ): + # ugly + can_change = {} + cannot_change = {} + for history in histories: + for dataset in history.active_datasets: + if not trans.app.security_agent.allow_action( send_to_user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset ): + if trans.app.security_agent.allow_action( user, trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=dataset ): + if p.action and p.action == "update": + trans.app.security_agent.associate_components( dataset=dataset, permissions=( send_to_group, [ trans.app.security_agent.permitted_actions.DATASET_ACCESS ] ) ) + elif history not in can_change: + can_change[history] = [ dataset ] + else: + can_change[history].append( dataset ) + else: + if p.action and p.action == "update": + pass # don't change stuff that the user doesn't have permission to change + elif history not in cannot_change: + cannot_change[history] = [ dataset ] + else: + cannot_change[history].append( dataset ) + if can_change or cannot_change: + return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err, can_change=can_change, cannot_change=cannot_change) for history in histories: new_history = history.copy( target_user=send_to_user ) new_history.name = history.name+" from "+user.email diff --git a/templates/history/share.mako b/templates/history/share.mako index 5e64e66756a..b854f256cdf 100644 --- a/templates/history/share.mako +++ b/templates/history/share.mako @@ -1,6 +1,7 @@ <%inherit file="/base.mako"/> <%def name="title()">Share histories +%if not can_change and not cannot_change:
                  Share Histories
                  ${data.display_name()} +
                  + + view or edit attributes + +
                  + + ${data.display_name()} +
                  ${data.ext} ${data.dbkey} ${data.info}
                  @@ -27,4 +28,85 @@
                  -
                  \ No newline at end of file +
            +%else: + +
            + %for history in histories: + + %endfor + +
            + The history or histories you've chosen to share contain datasets that the user you're sharing with does not have permission to access. These datasets are shown below. Datasets which the user already has permission to access are not shown. +
            +

            + %if can_change: +

            + The following datasets can be shared with ${email} by updating their permissions: +

            + + + %for history, datasets in can_change.items(): + + + + + %endfor +
            HistoriesDatasets
            ${history.name} + %for dataset in datasets: + ${dataset.name}
            + %endfor +
            +

            +

            + %endif + %if cannot_change: +

            + The following datasets cannot be shared with ${email} because you do not have permission to change the permissions on them. +

            + + + %for history, datasets in cannot_change.items(): + + + + + %endfor +
            HistoriesDatasets
            ${history.name} + %for dataset in datasets: + ${dataset.name}
            + %endfor +
            +

            +

            + %endif +

            + How would you like to proceed? +

            + %if can_change: + Change permissions + %if cannot_change: + (where possible) + %endif +
            + %endif + Share anyway + %if can_change: + (don't change any permissions) + %endif +
            + Don't share
            +
            +
            +

            +
            +%endif From 8e17384a79d5bdf77daf82d0dbe68de1715f919c Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 18 Sep 2008 15:18:12 -0400 Subject: [PATCH 55/94] Upgrade to SQLAlchemy 0.4.7 and correct conflicts in ~/model/__init__.py. --- eggs.ini | 4 +- lib/galaxy/app.py | 2 +- lib/galaxy/model/__init__.py | 48 +++---------- lib/galaxy/model/custom_types.py | 17 +++-- lib/galaxy/model/mapping.py | 68 +++++++++---------- lib/galaxy/model/orm/__init__.py | 7 ++ lib/galaxy/model/orm/ext/__init__.py | 3 + lib/galaxy/model/orm/ext/assignmapper.py | 62 +++++++++++++++++ lib/galaxy/web/controllers/admin.py | 8 +-- lib/galaxy/web/framework/__init__.py | 2 +- .../webapps/reports/controllers/jobs.py | 2 +- .../webapps/reports/controllers/system.py | 5 +- .../webapps/reports/controllers/users.py | 2 +- scripts/cleanup_datasets/cleanup_datasets.py | 4 +- test/functional/test_history_functions.py | 5 +- .../functional/test_security_and_libraries.py | 2 +- tools/stats/grouping.py | 2 +- 17 files changed, 144 insertions(+), 99 deletions(-) create mode 100644 lib/galaxy/model/orm/__init__.py create mode 100644 lib/galaxy/model/orm/ext/__init__.py create mode 100644 lib/galaxy/model/orm/ext/assignmapper.py diff --git a/eggs.ini b/eggs.ini index 6b918a7f324..1950ce4aa05 100644 --- a/eggs.ini +++ b/eggs.ini @@ -39,7 +39,7 @@ PasteDeploy = 1.3.1 PasteScript = 1.3.6 Routes = 1.6.3 simplejson = 1.5 -SQLAlchemy = 0.3.11 +SQLAlchemy = 0.4.7p1 Tempita = 0.1 twill = 0.9 WebError = 0.8a @@ -84,7 +84,7 @@ PasteDeploy = http://cheeseshop.python.org/packages/source/P/PasteDeploy/PasteDe PasteScript = http://cheeseshop.python.org/packages/source/P/PasteScript/PasteScript-1.3.6.tar.gz Routes = http://pypi.python.org/packages/source/R/Routes/Routes-1.6.3.tar.gz simplejson = http://cheeseshop.python.org/packages/source/s/simplejson/simplejson-1.5.tar.gz -SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.3.11.tar.gz +SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.4.7p1.tar.gz Tempita = http://pypi.python.org/packages/source/T/Tempita/Tempita-0.1.tar.gz twill = http://darcs.idyll.org/~t/projects/twill-0.9.tar.gz WebError = http://pypi.python.org/packages/source/W/WebError/WebError-0.8a.tar.gz diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 1964945012e..eb26d6a9acd 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -21,7 +21,7 @@ class UniverseApplication( object ): if self.config.database_connection: db_url = self.config.database_connection else: - db_url = "sqlite://%s?isolation_level=IMMEDIATE" % self.config.database + db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database # Setup the database engine and ORM self.model = galaxy.model.mapping.init( self.config.file_path, db_url, diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 342ee4636ef..4ab41942f4e 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -103,28 +103,9 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -<<<<<<< local -class HistoryDatasetAssociation( object ): - def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, - dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, - parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): - self.name = name or "Unnamed dataset" - self.id = id - self.hid = hid - self.info = info - self.blurb = blurb - self.peek = peek - self.extension = extension - self.designation = designation - self.metadata = metadata or dict() - self.dbkey = dbkey - self.deleted = deleted - self.visible = visible - # Relationships -======= class GroupDatasetAssociation( object ): def __init__( self, group, dataset, permitted_actions=[] ): - if isinstance( group, GroupDatasetAssociation ) or \ + if isinstance( group, GroupDatasetAssociation ) or \ isinstance( group, DefaultUserGroupAssociation ) or \ isinstance( group, DefaultHistoryGroupAssociation ): group = group.group @@ -182,16 +163,9 @@ class DefaultHistoryGroupAssociation( object ): isinstance( group, DefaultUserGroupAssociation ) or \ isinstance( group, DefaultHistoryGroupAssociation ): group = group.group ->>>>>>> other self.history = history -<<<<<<< local - if not dataset and create_dataset: - dataset = Dataset() - dataset.flush() - self.dataset = dataset - self.parent_id = parent_id - self.validation_errors = validation_errors - self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.group = group + self.permitted_actions = permitted_actions @property def ext( self ): @@ -342,8 +316,6 @@ class DefaultHistoryGroupAssociation( object ): for child in self.children: child.mark_deleted() - - class History( object ): def __init__( self, id=None, name=None, user=None ): self.id = id @@ -415,10 +387,6 @@ class History( object ): # # Relationships # self.history = history # self.datasets = [] -======= - self.group = group - self.permitted_actions = permitted_actions ->>>>>>> other class Dataset( object ): states = Bunch( NEW = 'new', @@ -528,7 +496,7 @@ class DatasetInstance( object ): self.extension = extension self.dbkey = dbkey self.designation = designation - self._metadata = metadata or dict() + self.metadata = metadata or dict() self.deleted = deleted self.visible = visible # Relationships @@ -559,9 +527,9 @@ class DatasetInstance( object ): 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 ) + if not hasattr( self, '_metadata_collection' ): + self._metadata_collection = MetadataCollection( self, self.datatype.metadata_spec ) + return self._metadata_collection def set_metadata( self, bunch ): # Needs to accept a MetadataCollection, a bunch, or a dict self._metadata = dict( bunch.items() ) @@ -587,6 +555,8 @@ class DatasetInstance( object ): dbkey = property( get_dbkey, set_dbkey ) def change_datatype( self, new_ext ): self.clear_associated_files() + if hasattr( self, '_metadata_collection' ): + del self._metadata_collection datatypes_registry.change_datatype( self, new_ext ) def get_size( self ): """Returns the size of the data on disk""" diff --git a/lib/galaxy/model/custom_types.py b/lib/galaxy/model/custom_types.py index f537a93f463..72d6b79075c 100644 --- a/lib/galaxy/model/custom_types.py +++ b/lib/galaxy/model/custom_types.py @@ -16,16 +16,15 @@ class JSONType( TypeDecorator ): self.mutable = mutable super( JSONType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_bind_param( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) - return self.jsonifyer.loads( str(buf) ) - - def convert_bind_param( self, value, dialect ): + return self.jsonifyer.dumps( value ) + + def process_result_value( self, value, dialect ): if value is None: return None - return self.impl.convert_bind_param( self.jsonifyer.dumps(value), dialect ) + return self.jsonifyer.loads( str( value ) ) def copy_value( self, value ): if self.mutable: @@ -58,10 +57,10 @@ class MetadataType( JSONType ): self.mutable = mutable super( MetadataType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_result_value( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) + buf = value ret = None try: ret = self.pickler.loads( str(buf) ) @@ -75,7 +74,7 @@ class MetadataType( JSONType ): class TrimmedString( TypeDecorator ): impl = String - def convert_bind_param( self, value, dialect ): + def process_bind_param( self, value, dialect ): """Automatically truncate string values""" if self.impl.length and value is not None: value = value[0:self.impl.length] diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 5beab407c4c..f0262731996 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -5,24 +5,21 @@ are encapsulated here. import logging log = logging.getLogger( __name__ ) -import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) - import sys import datetime -from sqlalchemy.ext.sessioncontext import SessionContext -from sqlalchemy.ext.assignmapper import assign_mapper -from sqlalchemy.ext.orderinglist import ordering_list - -from sqlalchemy import * from galaxy.model import * +from galaxy.model.orm import * +from galaxy.model.orm.ext.assignmapper import * from galaxy.model.custom_types import * from galaxy.util.bunch import Bunch from galaxy.security import GalaxyRBACAgent -metadata = DynamicMetaData( threadlocal=False ) -context = SessionContext( create_session ) +metadata = MetaData() +context = Session = scoped_session( sessionmaker( autoflush=False, transactional=False ) ) + +# For backward compatibility with "context.current" +context.current = Session dialect_to_egg = { "sqlite" : "pysqlite>=2", @@ -224,15 +221,15 @@ Job.table = Table( "job", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), Column( "tool_id", String( 255 ) ), - Column( "tool_version", String, default="1.0.0" ), + Column( "tool_version", TEXT, default="1.0.0" ), Column( "state", String( 64 ) ), Column( "info", TrimmedString( 255 ) ), - Column( "command_line", String() ), + Column( "command_line", TEXT ), Column( "param_filename", String( 1024 ) ), Column( "runner_name", String( 255 ) ), - Column( "stdout", String() ), - Column( "stderr", String() ), - Column( "traceback", String() ), + Column( "stdout", TEXT ), + Column( "stderr", TEXT ), + Column( "traceback", TEXT ), Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True, nullable=True ), Column( "job_runner_name", String( 255 ) ), Column( "job_runner_external_id", String( 255 ) ) ) @@ -292,7 +289,7 @@ StoredWorkflow.table = Table( "stored_workflow", metadata, Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=False ), Column( "latest_workflow_id", Integer, ForeignKey( "workflow.id", use_alter=True, name='stored_workflow_latest_workflow_id_fk' ), index=True ), - Column( "name", String ), + Column( "name", TEXT ), Column( "deleted", Boolean, default=False ), ) @@ -301,7 +298,7 @@ Workflow.table = Table( "workflow", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True, nullable=False ), - Column( "name", String ), + Column( "name", TEXT ), Column( "has_cycles", Boolean ), Column( "has_errors", Boolean ) ) @@ -312,8 +309,8 @@ WorkflowStep.table = Table( "workflow_step", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "workflow_id", Integer, ForeignKey( "workflow.id" ), index=True, nullable=False ), Column( "type", String(64) ), - Column( "tool_id", String ), - Column( "tool_version", String ), # Reserved for future + Column( "tool_id", TEXT ), + Column( "tool_version", TEXT ), # Reserved for future Column( "tool_inputs", JSONType ), Column( "tool_errors", JSONType ), Column( "position", JSONType ), @@ -326,8 +323,8 @@ WorkflowStepConnection.table = Table( "workflow_step_connection", metadata, Column( "id", Integer, primary_key=True ), Column( "output_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), Column( "input_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), - Column( "output_name", String ), - Column( "input_name", String) + Column( "output_name", TEXT ), + Column( "input_name", TEXT) ) StoredWorkflowUserShareAssociation.table = Table( "stored_workflow_user_share_connection", metadata, @@ -352,9 +349,7 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab dataset=relation( Dataset, primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ), lazy=False ), - history=relation( - History, - primaryjoin=( History.table.c.id == HistoryDatasetAssociation.table.c.history_id ) ), + # .history defined in History mapper copied_to_history_dataset_associations=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), @@ -415,11 +410,11 @@ assign_mapper( context, Group, Group.table, assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, properties=dict( user=relation( User, backref = "groups" ), - group=relation( Group, backref = "users" ) ) ) + group=relation( Group, backref = "members" ) ) ) assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "groups" ), - group=relation( Group, backref = "datasets" ) ) ) + group=relation( Group, backref = "group_datasets" ) ) ) assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, properties=dict( user=relation( User, backref = "default_groups" ), @@ -555,11 +550,12 @@ def db_next_hid( self ): Override __next_hid to generate from the database in a concurrency safe way. """ - conn = self.table.engine.contextual_connect() + conn = object_session( self ).connection() + table = self.table trans = conn.begin() try: - next_hid = select( [self.c.hid_counter], self.c.id == self.id, for_update=True ).scalar() - self.table.update( self.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) trans.commit() return next_hid except: @@ -588,17 +584,21 @@ def init( file_path, url, engine_options={}, create_tables=False ): # Create the database engine engine = create_engine( url, **engine_options ) # Connect the metadata to the database. - metadata.connect( engine ) - ## metadata.engine.echo = True + metadata.bind = engine + # Clear any existing contextual sessions and reconfigure + Session.remove() + Session.configure( bind=engine ) # Create tables if needed if create_tables: metadata.create_all() # metadata.engine.commit() # Pack everything into a bunch result = Bunch( **globals() ) - result.engine = metadata.engine - result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) - result.context = context + result.engine = engine + result.flush = lambda *args, **kwargs: Session.flush( *args, **kwargs ) + result.session = Session + # For backward compatibility with "model.context.current" + result.context = Session result.create_tables = create_tables #load local galaxy security policy result.security_agent = GalaxyRBACAgent( result ) diff --git a/lib/galaxy/model/orm/__init__.py b/lib/galaxy/model/orm/__init__.py new file mode 100644 index 00000000000..433e1e26f45 --- /dev/null +++ b/lib/galaxy/model/orm/__init__.py @@ -0,0 +1,7 @@ +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) + +from sqlalchemy import * +from sqlalchemy.orm import * + +from sqlalchemy.ext.orderinglist import ordering_list diff --git a/lib/galaxy/model/orm/ext/__init__.py b/lib/galaxy/model/orm/ext/__init__.py new file mode 100644 index 00000000000..230b8aec908 --- /dev/null +++ b/lib/galaxy/model/orm/ext/__init__.py @@ -0,0 +1,3 @@ +""" +Galaxy specific SQLAlchemy extensions. +""" \ No newline at end of file diff --git a/lib/galaxy/model/orm/ext/assignmapper.py b/lib/galaxy/model/orm/ext/assignmapper.py new file mode 100644 index 00000000000..d2b7e2ca7b4 --- /dev/null +++ b/lib/galaxy/model/orm/ext/assignmapper.py @@ -0,0 +1,62 @@ +""" +This is similar to the assignmapper extensions in SQLAclhemy 0.3 and 0.4 but +with some compatibility fixes. It assumes that the session is a ScopedSession, +and thus has the "mapper" method to attach contextual mappers to a class. It +adds additional query and session methods to the class to support the +SQLAlchemy 0.3 style of access. The following methods which would normally be +accessed through "Object.query().method()" are available directly through the +object: + + 'get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances' + +Additionally, the following Session methods, which normally accept an instance +or list of instances, are available directly through the objects, e.g. +"Session.flush( [instance] )" can be performed as "instance.flush()": + + 'refresh', 'expire', 'delete', 'expunge', 'update' +""" + +__all__ = [ 'assign_mapper' ] + +from sqlalchemy import util, exceptions +import types +from sqlalchemy.orm import mapper, Query + +def _monkeypatch_query_method( name, session, class_ ): + def do(self, *args, **kwargs): + ## util.warn_deprecated('Query methods on the class are deprecated; use %s.query.%s instead' % (class_.__name__, name)) + return getattr( class_.query, name)(*args, **kwargs) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, classmethod(do)) + +def _monkeypatch_session_method(name, session, class_, make_list=False): + def do(self, *args, **kwargs): + if make_list: + self = [ self ] + return getattr(session, name)( self, *args, **kwargs ) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, do) + +def assign_mapper( session, class_, *args, **kwargs ): + m = class_.mapper = session.mapper( class_, *args, **kwargs ) + for name in ('get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances'): + _monkeypatch_query_method(name, session, class_) + for name in ('refresh', 'expire', 'delete', 'expunge', 'update'): + _monkeypatch_session_method(name, session, class_) + for name in ( 'flush', ): + _monkeypatch_session_method( name, session, class_, make_list=True ) + return m diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 958e6234663..c7035dd98eb 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -6,7 +6,7 @@ from galaxy.security import RBACAgent import galaxy.model from xml.sax.saxutils import escape, unescape import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging @@ -251,7 +251,7 @@ class Admin( BaseController ): # 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: + for user_group_assoc in group.members: if user_group_assoc.user_id not in members: user = galaxy.model.User.get( user_group_assoc.user_id ) # Delete DefaultUserGroupAssociations @@ -272,7 +272,7 @@ class Admin( BaseController ): # 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: + if user not in group.members: 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 ) @@ -405,7 +405,7 @@ class Admin( BaseController ): group_id = params.group_id group = galaxy.model.Group.get( group_id ) # Remove members and all associations - for user_group_assoc in group.users: + for user_group_assoc in group.members: user = galaxy.model.User.get( user_group_assoc.user_id ) # Delete DefaultUserGroupAssociations for default_user_group_association in user.default_groups: diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 2be8810b68e..20bda3c2d8e 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -24,7 +24,7 @@ import mako.lookup pkg_resources.require( "simplejson" ) import simplejson -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) from sqlalchemy import desc import logging diff --git a/lib/galaxy/webapps/reports/controllers/jobs.py b/lib/galaxy/webapps/reports/controllers/jobs.py index bf7935d9647..a502ca48532 100644 --- a/lib/galaxy/webapps/reports/controllers/jobs.py +++ b/lib/galaxy/webapps/reports/controllers/jobs.py @@ -5,7 +5,7 @@ import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/reports/controllers/system.py b/lib/galaxy/webapps/reports/controllers/system.py index dc6d3b36b90..477b2084d03 100644 --- a/lib/galaxy/webapps/reports/controllers/system.py +++ b/lib/galaxy/webapps/reports/controllers/system.py @@ -2,8 +2,9 @@ import operator, os from datetime import datetime, timedelta from galaxy.webapps.reports.base.controller import * import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) -from sqlalchemy import eagerload, desc +pkg_resources.require( "SQLAlchemy >= 0.4" ) +from sqlalchemy.orm import eagerload +from sqlalchemy import desc import logging log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index b99546f14dd..185a18107c3 100644 --- a/lib/galaxy/webapps/reports/controllers/users.py +++ b/lib/galaxy/webapps/reports/controllers/users.py @@ -3,7 +3,7 @@ import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) diff --git a/scripts/cleanup_datasets/cleanup_datasets.py b/scripts/cleanup_datasets/cleanup_datasets.py index 0d653981d00..97ca3f8e8f8 100644 --- a/scripts/cleanup_datasets/cleanup_datasets.py +++ b/scripts/cleanup_datasets/cleanup_datasets.py @@ -13,8 +13,8 @@ from galaxy import eggs import galaxy.model.mapping import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) -from sqlalchemy import eagerload +pkg_resources.require( "SQLAlchemy >= 0.4" ) +from sqlalchemy.orm import eagerload assert sys.version_info[:2] >= ( 2, 4 ) diff --git a/test/functional/test_history_functions.py b/test/functional/test_history_functions.py index 24663bf4e77..8b0b12819fa 100644 --- a/test/functional/test_history_functions.py +++ b/test/functional/test_history_functions.py @@ -48,7 +48,10 @@ class TestHistory( TwillTestCase ): """Testing sharing a history with another user""" self.upload_file('1.bed', dbkey='hg18') id, name, email = self.share_history() - self.check_page_for_string( 'History (%s) has been shared with: %s' %(name, email) ) + try: + self.check_page_for_string( 'History (%s) has been shared with: %s' %(name, email) ) + except TwillAssertionError: + self.check_page_for_string( "The history or histories you've chosen to share contain datasets that the user you're sharing with does not have permission to access." ) self.logout() self.login( email='test2@bx.psu.edu' ) self.view_stored_histories() diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 748684e696f..031eef67b61 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -85,7 +85,7 @@ class TestHistory( TwillTestCase ): def test_20_create_library( self ): """Testing creating new library""" - self.create_library( name='New Test Library', description='New test Library Description' ) + self.create_library( name='New Test Library', description='New Test Library Description' ) self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Library" ) diff --git a/tools/stats/grouping.py b/tools/stats/grouping.py index d21a6da6f4e..a26fb033f0b 100644 --- a/tools/stats/grouping.py +++ b/tools/stats/grouping.py @@ -90,7 +90,7 @@ def main(): for ii, line in enumerate( file( tmpfile.name )): if line and not line.startswith( '#' ): - line = line.strip() + line = line.rstrip( '\r\n' ) try: fields = line.split("\t") item = fields[group_col] From 0cfd2717f0e84b7bfa8dc350f5ef46bcda5016a1 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 19 Sep 2008 10:29:37 -0400 Subject: [PATCH 56/94] Need uselist=False for these backrefs in alchemy 0.4... are there others? --- lib/galaxy/model/mapping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index f0262731996..347010db27e 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -353,11 +353,11 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab copied_to_history_dataset_associations=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ), copied_to_library_folder_dataset_associations=relation( LibraryFolderDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), - backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id], uselist=False ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), From 1e5d2759f832480c349b4fd4886429121c45e3a8 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 19 Sep 2008 15:37:36 -0400 Subject: [PATCH 57/94] =?UTF-8?q?Change=20the=20way=20GMAJ=20peeks=20are?= =?UTF-8?q?=20generated.=20A=20non-escaped=20©=5Faccess=5Ffrom=20was?= =?UTF-8?q?=20being=20modified=20to=20'=C2=A9=5Faccess=5Ffrom'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/galaxy/datatypes/images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 1db5291a5a2..78bf6964e81 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -5,7 +5,7 @@ Image classes import data import logging from galaxy.datatypes.sniff import * -from urllib import urlencode +from urllib import urlencode, quote_plus import zipfile log = logging.getLogger(__name__) @@ -112,7 +112,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, 'copy_access_from': dataset.id } ) + "posturl": quote_plus( "history_add_to?%s" % "&".join( [ "%s=%s" % ( key, value ) for key, value in { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id }.items() ] ) ) } class_name = "edu.psu.bx.gmaj.MajApplet.class" archive = "/static/gmaj/gmaj.jar" @@ -186,7 +186,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, 'copy_access_from': dataset.id } ), + "posturl": quote_plus( "history_add_to?%s" % "&".join( [ "%s=%s" % ( key, value ) for key, value in { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id }.items() ] ) ), "noseq": "true" } class_name = "edu.psu.cse.bio.laj.LajApplet.class" From bf540d2628ea06ac546c5754b794327fc0977fbe Mon Sep 17 00:00:00 2001 From: Anton Nekrutenko Date: Fri, 19 Sep 2008 16:28:11 -0400 Subject: [PATCH 58/94] Unseq genomes for oct --- static/images/welcomePhoto.jpg | Bin 76711 -> 31819 bytes static/welcome.html | 23 +++++++++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/static/images/welcomePhoto.jpg b/static/images/welcomePhoto.jpg index 91abdf314960f806906f2312974db26934573a63..33fc6a470dfe04a4d42b2bd19754ba358f585ee6 100644 GIT binary patch delta 30643 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%0DmnRHzj>!E){9f=%`;rl@1az) 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%0D|rDM9ZWkDoB6vxZhlFV{3 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 Bp_Kpt diff --git a/static/welcome.html b/static/welcome.html index 7a5064c6af7..5bd2ab245cd 100644 --- a/static/welcome.html +++ b/static/welcome.html @@ -10,28 +10,27 @@
            -
            - Galaxy and 2008 Meeting Season +
            + Workflows are finally here!
            + Watch how you can (Click link to play)...
              -
            • Beyond Genome | San Francisco | June 9 - 11
            • -
            • ISMB 2008 | Toronto | July 19 - 23
            • -
            • Genome Informatics | Hinxton, UK | September 10 - 14
            • -
            • ASHG | Philadelphia | November 11 - 15
            • -
            +
          • Create Workflows by Example: Convert your Galaxy History into a Workflow.
          • +
          • Edit Workflows: I want to repeat an analysis...but...with different parameters.
          • +
          • Workflows from scratch: Drag, drag, drag...
          • +
            - Download new Galaxy brochure here. -
            - + For more screencasts click here. +

            - Unsequenced Genomes of the World | June 2008 + Unsequenced Genomes of the World | October 2008


            - Przewalski's Horse (Equus przewalskii) | Escondido, California + Costa's hummingbird (Calypte costae) | Kings Canyon NP, California

            From 830f545f1c3b1e31d22d9960d5acf01e8d235057 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 19 Sep 2008 16:29:55 -0400 Subject: [PATCH 59/94] James, please review. Is the use of save_or_update okay? Corrected another 0.4 backref assignment bug that Anton found in workflow. --- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/model/mapping.py | 2 +- lib/galaxy/web/controllers/workflow.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 4ab41942f4e..cbeac083a1d 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -936,7 +936,7 @@ class WorkflowStep( object ): self.tool_inputs = None self.tool_errors = None self.position = None - self.input_connections = None + self.input_connections = [] self.config = None class WorkflowStepConnection( object ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 347010db27e..222de251097 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -364,7 +364,7 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab children=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ) + backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ) ) ) assign_mapper( context, Dataset, Dataset.table, diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index ac1a7f10ab6..d50cb969c38 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -64,7 +64,7 @@ class WorkflowController( BaseController ): share.stored_workflow = stored share.user = other session = trans.sa_session - session.save( share ) + session.save_or_update( share ) session.flush() trans.set_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) ) return self.list( trans ) @@ -107,7 +107,7 @@ class WorkflowController( BaseController ): new_stored.user = user # Persist session = trans.sa_session - session.save( new_stored ) + session.save_or_update( new_stored ) session.flush() # Display the management page trans.set_message( 'Clone created with name "%s"' % new_stored.name ) @@ -132,7 +132,7 @@ class WorkflowController( BaseController ): stored_workflow.latest_workflow = workflow # Persist session = trans.sa_session - session.save( stored_workflow ) + session.save_or_update( stored_workflow ) session.flush() # Display the management page trans.set_message( "Workflow '%s' created" % stored_workflow.name ) @@ -309,7 +309,7 @@ class WorkflowController( BaseController ): workflow.stored_workflow = stored stored.latest_workflow = workflow # Persist - trans.sa_session.save( stored ) + trans.sa_session.save_or_update( stored ) trans.sa_session.flush() # Return something informative errors = [] @@ -434,7 +434,7 @@ class WorkflowController( BaseController ): stored.name = workflow_name workflow.stored_workflow = stored stored.latest_workflow = workflow - trans.sa_session.save( stored ) + trans.sa_session.save_or_update( stored ) trans.sa_session.flush() # Index page with message return trans.show_message( "Workflow '%s' created from current history." % workflow_name ) @@ -740,4 +740,4 @@ def cleanup_param_values( inputs, values ): prefix = "%s|" % ( key ) cleanup( prefix, input.cases[current_case].inputs, group_values ) cleanup( "", inputs, values ) - return associations \ No newline at end of file + return associations From b7e69706a12b6c42c7304f7a61ff2eafc3d56d3e Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 23 Sep 2008 17:23:29 -0400 Subject: [PATCH 60/94] 1) Add a new tag to DataToolParameters to filter out datasets from inclusion in the select list that do not pass the check. The tag set looks something like . 2) Fixed some bugs in how we associate datasets with groups and permissions, specifically when a user logs in after creating a history with datasets while not authenticated. 3) Added a new active_histories mapper to User. 4) The DataToolParameter's dataset_collecter is now sent history.active_datasets rather than history.datasets. --- lib/galaxy/model/mapping.py | 1 + lib/galaxy/security/__init__.py | 64 +++++++++++++---------- lib/galaxy/tools/__init__.py | 5 +- lib/galaxy/tools/parameters/basic.py | 77 +++++++++++++++++++--------- tools/data_destination/epigraph.xml | 10 +++- 5 files changed, 100 insertions(+), 57 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 222de251097..afdaf27c4c3 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -399,6 +399,7 @@ assign_mapper( context, History, History.table, assign_mapper( context, User, User.table, properties=dict( histories=relation( History, backref="user", order_by=desc(History.table.c.update_time) ), + active_histories=relation( History, primaryjoin=( ( History.table.c.user_id == User.table.c.id ) & ( not_( History.table.c.deleted ) ) ), order_by=desc( History.table.c.update_time ) ), stored_workflow_menu_entries=relation( StoredWorkflowMenuEntry, backref="user", cascade="all, delete-orphan", collection_class=ordering_list( 'order_index' ) ) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 4bd8cf2fe44..425b1600598 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -142,7 +142,10 @@ class GalaxyRBACAgent( RBACAgent ): return intersect def get_group( self, id ): return self.model.Group.get( id ) - raise 'No valid method of retrieving requested group %s' % ( id ) + raise 'No valid method of retrieving requested group by id %s' % ( str( id ) ) + def get_group_by_name( self, name ): + return self.model.Group.get_by( name=name ) + raise 'No valid method of retrieving requested group by name %s' % ( str( name ) ) def create_group( self, **kwd ): rval = self.model.Group( **kwd ) rval.flush() @@ -181,14 +184,16 @@ class GalaxyRBACAgent( RBACAgent ): if permissions is None: permissions = [ ( self.create_private_user_group( user ), self.permitted_actions.__dict__.values() ) ] if permissions is not None: - for assoc in user.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() + # Delete all of the previous DefaultUserGroupAssociation + for duga in user.default_groups: #this is the association not the actual group + duga.delete() + duga.flush() + # Add the new DefaultUserGroupAssociation for group, permitted_actions in permissions: - assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) - assoc.flush() + duga = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) + duga.flush() if history: - for history in user.histories: + for history in user.active_histories: self.history_set_default_access( history, permissions=permissions, dataset=dataset ) def user_get_default_access( self, user ): return [ ( duga.group, duga.permitted_actions ) for duga in user.default_groups ] @@ -199,22 +204,26 @@ class GalaxyRBACAgent( RBACAgent ): else: permissions = [ ( self.get_public_group(), self.permitted_actions.__dict__.values() ) ] if permissions is not None: - for assoc in history.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() + # Delete all of the previous DefaultHistoryGroupAssociations + for dhga in history.default_groups: #this is the association not the actual group + dhga.delete() + dhga.flush() + # Add the new DefaultHistoryGroupAssociations for group, permitted_actions in permissions: - assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) - assoc.flush() + dhga = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) + dhga.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: + for hda in history.datasets: + for hda2 in hda.dataset.history_associations: + if history.user and hda2.history not in history.user.active_histories: # This will occur when a user logs in and has datasets in their previously-public history. - self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) - break + self.set_dataset_permissions( hda.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) + #break else: - if self.allow_action( history.user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=data.dataset ): - self.set_dataset_permissions( data.dataset, permissions ) + # At this point, we will have already created a gda for the public group and delete_assocs + # is set to False in order to preserve it. + if self.allow_action( history.user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=hda.dataset ): + self.set_dataset_permissions( hda.dataset, permissions, delete_existing_assocs=False ) def history_get_default_access( self, history ): return [ ( dhga.group, dhga.permitted_actions ) for dhga in history.default_groups ] def get_public_group( self ): @@ -223,7 +232,7 @@ class GalaxyRBACAgent( RBACAgent ): return self.model.Group.set_public_group( group ) def guess_public_group( self ): return self.model.Group.guess_public_group() - def set_dataset_permissions( self, dataset, permissions ): + def set_dataset_permissions( self, dataset, permissions, delete_existing_assocs=True ): """ Apply permissions (a list of (group, permitted_action) tuples) to a dataset, removing any existing permissions. permissions can @@ -231,10 +240,11 @@ class GalaxyRBACAgent( RBACAgent ): """ if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset - for group_dataset_assoc in dataset.groups: - group_dataset_assoc.delete() - group_dataset_assoc.flush() - if len( permissions ) and isinstance( permissions[0], self.model.GroupDatasetAssociation ): + if delete_existing_assocs: + for gda in dataset.groups: + gda.delete() + gda.flush() + if permissions and isinstance( permissions[0], self.model.GroupDatasetAssociation ): permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] for ptuple in permissions: self.associate_components( dataset=dataset, permissions=ptuple ) @@ -259,9 +269,9 @@ class GalaxyRBACAgent( RBACAgent ): return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) def check_folder_contents( self, user, entry ): """ - Return true if there are any datasets under 'folder' that the - user has access permission on. We do this a lot and it's a - pretty inefficient method, optimizations are welcomed. + Return true if there are any datasets under 'folder' that the + user has access permission on. We do this a lot and it's a + pretty inefficient method, optimizations are welcomed. """ if isinstance( entry, self.model.Library ): return self.check_folder_contents( user, entry.root_folder ) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 8cb7c044719..23c6d8074df 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1067,7 +1067,7 @@ class Tool: redirect_url_params = redirect_url_params.replace( "\n", " " ).replace( "\r", " " ) return redirect_url_params - def parse_redirect_url( self, inp_data, param_dict ): + def parse_redirect_url( self, data, param_dict ): """Parse the REDIRECT_URL tool param""" # Tools that send data to an external application via a redirect must include the following 3 # tool params: @@ -1087,9 +1087,6 @@ class Tool: rup_dict[ p_name ] = p_val DATA_URL = param_dict.get( 'DATA_URL', None ) assert DATA_URL is not None, "DATA_URL parameter missing in tool config." - # Get the dataset - there should only be 1 - for name in inp_data.keys(): - data = inp_data[ name ] DATA_URL += "/%s/display" % str( data.id ) redirect_url += "?DATA_URL=%s" % DATA_URL # Add the redirect_url_params to redirect_url diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 6a2f278f47b..be656b53342 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -3,16 +3,13 @@ Basic tool parameters. """ import logging, string, sys, os - from elementtree.ElementTree import XML, Element - from galaxy import config, datatypes, util from galaxy.web import form_builder - import validation, dynamic_options - # For BaseURLToolParameter from galaxy.web import url_for +import galaxy.model log = logging.getLogger(__name__) @@ -985,6 +982,9 @@ class DataToolParameter( ToolParameter ): TODO: There should be an alternate display that allows single selects to be displayed as radio buttons and multiple selects as a set of checkboxes + TODO: The following must be fixed to test correctly for the new security_check tag in the DataToolParameter ( the last test below is broken ) + Nate's next passs at the dataset security stuff will dramatically alter this anyway. + >>> # Mock up a history (not connected to database) >>> from galaxy.model import History, HistoryDatasetAssociation, User, Group >>> from galaxy.util.bunch import Bunch @@ -997,13 +997,10 @@ class DataToolParameter( ToolParameter ): >>> group.flush() >>> Group.public_id = group.id >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) - >>> security_agent.set_dataset_permissions( dataset1, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) >>> security_agent.set_dataset_permissions( dataset2, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) - >>> security_agent.set_dataset_permissions( dataset3, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) - >>> security_agent.set_dataset_permissions( dataset4, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) >>> security_agent.set_dataset_permissions( dataset5, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> hist.add_dataset( dataset1 ) @@ -1011,13 +1008,13 @@ class DataToolParameter( ToolParameter ): >>> hist.add_dataset( dataset3 ) >>> hist.add_dataset( dataset4 ) >>> hist.add_dataset( dataset5 ) - >>> p = DataToolParameter( None, XML( '' ) ) + >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah + >>> print p.security_dict + {'test': ['access']} >>> print p.get_html( trans=Bunch( history=hist, user=None, app=Bunch( security_agent = security_agent ) ) ) """ @@ -1048,6 +1045,16 @@ class DataToolParameter( ToolParameter ): else: self.options = dynamic_options.DynamicOptions( options, self ) self.is_dynamic = self.options is not None + self.security_dict = {} + for security_check in elem.findall( "security_check" ): + group = security_check.get( 'group', None ) + assert group is not None, "security_check elements require a group attribute" + action = security_check.get( 'action', None ) + assert action is not None, "security_check elements require an action attribute" + try: + self.security_dict[ group ].append( action ) + except: + self.security_dict[ group ] = [ action ] def get_html_field( self, trans=None, value=None, other_values={} ): filter_value = None @@ -1064,34 +1071,54 @@ class DataToolParameter( ToolParameter ): value = [ value ] field = form_builder.SelectField( self.name, self.multiple, None, self.refresh_on_change ) # CRUCIAL: the dataset_collector function needs to be local to DataToolParameter.get_html_field() - def dataset_collector( datasets, parent_hid ): - for i, data in enumerate( datasets ): + def dataset_collector( hdas, parent_hid ): + for i, hda in enumerate( hdas ): if parent_hid is not None: 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 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: + hid = str( hda.hid ) + if not hda.dataset.state == galaxy.model.Dataset.states.ERROR and hda.visible and trans.app.security_agent.allow_action( trans.user, hda.permitted_actions.DATASET_ACCESS, dataset=hda ): + if self.security_dict: + passed_security_check = True + for group_name, permitted_actions in self.security_dict.items(): + # Make sure the dataset belongs to the group + group = trans.app.security_agent.get_group_by_name( group_name ) + if not trans.app.security_agent.dataset_has_group( hda.dataset_id, group.id ): + passed_security_check = False + break + # Make sure the permitted_actions are available on the dataset for the group + group_dataset_permitted_actions = trans.app.security_agent.get_dataset_permissions( hda.dataset, group_id=group.id )[1] + actions_allowed = True + for action in permitted_actions: + if not action in group_dataset_permitted_actions: + actions_allowed = False + break + if not actions_allowed: + passed_security_check = False + break + if not passed_security_check: + continue + if self.options and hda.get_dbkey() != filter_value: continue - if isinstance( data.datatype, self.formats): - selected = ( value and ( data in value ) ) - field.add_option( "%s: %s" % ( hid, data.name[:30] ), data.id, selected ) + if isinstance( hda.datatype, self.formats): + selected = ( value and ( hda in value ) ) + field.add_option( "%s: %s" % ( hid, hda.name[:30] ), hda.id, selected ) else: for target_ext in self.extensions: - if target_ext in data.get_converter_types(): - datasets = data.get_converted_files_by_type( target_ext ) + if target_ext in hda.get_converter_types(): + datasets = hda.get_converted_files_by_type( target_ext ) if datasets: 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 ): + if not trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=hda.dataset ): continue - selected = ( value and ( data in value ) ) - field.add_option( "%s: (as %s) %s" % ( hid, target_ext, data.name[:30] ), data.id, selected ) + selected = ( value and ( hda.dataset in value ) ) + field.add_option( "%s: (as %s) %s" % ( hid, target_ext, hda.name[:30] ), hda.dataset.id, selected ) break #we only report the first valid converter, assume self.extensions is a priority list # Also collect children via association object - dataset_collector( data.children, hid ) - dataset_collector( history.datasets, None ) + dataset_collector( hda.children, hid ) + dataset_collector( history.active_datasets, None ) some_data = bool( field.options ) if some_data: if value is None or len( field.options ) == 1: diff --git a/tools/data_destination/epigraph.xml b/tools/data_destination/epigraph.xml index bb2bd9b1b5d..5f68e7bbc76 100644 --- a/tools/data_destination/epigraph.xml +++ b/tools/data_destination/epigraph.xml @@ -3,7 +3,8 @@ Genome analysis and prediction GENOME=${input1.dbkey} NAME=${input1.name} INFO=${input1.info} - + + @@ -12,6 +13,13 @@ + + .. class:: infomark + +**TIP:** Data sent to EpiGRAPH must be "publicly accessible" ( the **public** group must have permission to access the data ). Click the pencil icon in the history item to change the dataset's permissions if you are permitted to do so. + +----- + **What it does** This tool sends the selected dataset to EpiGRAPH for in-depth analysis and prediction. From 97dad70d8e992c3985fc59c198cfd2c501515238 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Wed, 24 Sep 2008 09:11:40 -0400 Subject: [PATCH 61/94] Fix for building redirect URL. --- lib/galaxy/tools/actions/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index c3c21af95ea..d5595db2af9 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -204,7 +204,10 @@ class DefaultToolAction( object ): # include something that can be retrieved from the params ( e.g., REDIRECT_URL ) to keep the job # from being queued. if 'REDIRECT_URL' in incoming: - redirect_url = tool.parse_redirect_url( inp_data, incoming ) + # Get the dataset - there should only be 1 + for name in inp_data.keys(): + dataset = inp_data[ name ] + redirect_url = tool.parse_redirect_url( dataset, incoming ) # Job should not be queued, so set state to ok job.state = JOB_OK job.info = "Redirected to: %s" % redirect_url From 9bda606dbc4c89d870932f861ab3d7a0516e5ec2 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 24 Sep 2008 16:02:52 -0400 Subject: [PATCH 62/94] Resolve conflicts in model/__init__.py from central. --- lib/galaxy/model/__init__.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 6eebe6c9cdd..932d4a5be96 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -103,7 +103,6 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -<<<<<<< local class GroupDatasetAssociation( object ): def __init__( self, group, dataset, permitted_actions=[] ): if isinstance( group, GroupDatasetAssociation ) or \ @@ -164,26 +163,6 @@ class DefaultHistoryGroupAssociation( object ): isinstance( group, DefaultUserGroupAssociation ) or \ isinstance( group, DefaultHistoryGroupAssociation ): group = group.group -======= -class HistoryDatasetAssociation( object ): - def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, - dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, - parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): - self.name = name or "Unnamed dataset" - self.id = id - self.hid = hid - self.info = info - self.blurb = blurb - self.peek = peek - self.extension = extension - self.designation = designation - self.metadata = metadata or dict() - if dbkey: #dbkey is stored in metadata, only set if non-zero, or else we could clobber one supplied by input 'metadata' - self.dbkey = dbkey - self.deleted = deleted - self.visible = visible - # Relationships ->>>>>>> other self.history = history self.group = group self.permitted_actions = permitted_actions From bc7dc21cdbdd08a7e5fcb5c7ec3a740934ff9664 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 24 Sep 2008 16:12:13 -0400 Subject: [PATCH 63/94] Fixed conflicts in templates. --- templates/history/options.mako | 6 ------ templates/root/tool_menu.mako | 25 +------------------------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/templates/history/options.mako b/templates/history/options.mako index 49b63182e90..56dab0377ca 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -16,14 +16,8 @@ %if len( history.active_datasets ) > 0:
          • Create a new empty history
          • %endif -<<<<<<< local
          • Construct workflow from the current history
          • -======= - %if app.config.enable_beta_features: -
          • Construct workflow from the current history
          • - %endif
          • Change default permitted actions for the current history
          • ->>>>>>> other
          • Share current history
          • %endif
          • Delete current history diff --git a/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index 14e20f96280..04af8526c4c 100644 --- a/templates/root/tool_menu.mako +++ b/templates/root/tool_menu.mako @@ -82,7 +82,6 @@ ## at least some workflows will appear here (the user should be able to ## configure which of their stored workflows appear in the tools menu). -<<<<<<< local
            @@ -92,16 +91,7 @@
            Manage workflows -======= -%if app.config.enable_beta_features: - %if t.user and t.user.stored_workflow_menu_entries: -
            -
            -
            - Your workflows ->>>>>>> other
            -<<<<<<< local %if t.user: %for m in t.user.stored_workflow_menu_entries:
            @@ -111,19 +101,6 @@ %endif
            -======= -
            -
            - %for m in t.user.stored_workflow_menu_entries: - - %endfor -
            -
            - %endif -%endif ->>>>>>> other
            @@ -133,4 +110,4 @@ - \ No newline at end of file + From 5b75630caeaae8d46f5868e90e92688c41e21cb9 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 25 Sep 2008 16:14:26 -0400 Subject: [PATCH 64/94] Migrate sqlalchemy queries to 0.4 by eliminating deprecated queries ( select_by, selectone_by, get_by all replaced by filter_by ). --- lib/galaxy/jobs/__init__.py | 7 +++--- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/model/mapping.py | 2 +- lib/galaxy/model/mapping_tests.py | 6 ++--- lib/galaxy/security/__init__.py | 8 +++---- lib/galaxy/web/controllers/admin.py | 5 +++-- lib/galaxy/web/controllers/library.py | 4 ++-- lib/galaxy/web/controllers/root.py | 22 ++++++++----------- lib/galaxy/web/controllers/user.py | 11 +++++----- lib/galaxy/web/controllers/workflow.py | 2 +- lib/galaxy/web/framework/__init__.py | 12 +++++----- .../webapps/reports/controllers/users.py | 11 ++-------- .../functional/test_security_and_libraries.py | 8 +++---- 13 files changed, 44 insertions(+), 56 deletions(-) diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 3cda6ce6c76..827d3db3d6d 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -97,11 +97,10 @@ class JobQueue( object ): model = self.app.model # Jobs in the NEW state won't be requeued unless we're tracking in the database if not self.track_jobs_in_database: - for job in model.Job.select( model.Job.c.state == model.Job.states.NEW ): + for job in model.Job.filter( model.Job.c.state==model.Job.states.NEW ).all(): log.debug( "no runner: %s is still in new state, adding to the jobs queue" %job.id ) self.queue.put( ( job.id, job.tool_id ) ) - for job in model.Job.select( (model.Job.c.state == model.Job.states.RUNNING) - | (model.Job.c.state == model.Job.states.QUEUED) ): + for job in model.Job.filter( (model.Job.c.state == model.Job.states.RUNNING) | (model.Job.c.state == model.Job.states.QUEUED) ).all(): if job.job_runner_name is not None: # why are we passing the queue to the wrapper? job_wrapper = JobWrapper( job.id, self.app.toolbox.tools_by_id[ job.tool_id ], self ) @@ -136,7 +135,7 @@ class JobQueue( object ): new_jobs = [] if self.track_jobs_in_database: model = self.app.model - for j in model.Job.select( model.Job.c.state == model.Job.states.NEW ): + for j in model.Job.filter( model.Job.c.state==model.Job.states.NEW ).all(): job = JobWrapper( j.id, self.app.toolbox.tools_by_id[ j.tool_id ], self ) new_jobs.append( job ) else: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 932d4a5be96..19279d5c319 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -139,7 +139,7 @@ class Group( object ): @classmethod def guess_public_group( cls ): # Retrieve from database and store public group id - group = Group.select_by( name='public' )[0] + group = Group.filter_by( name='public' ).first() cls.set_public_group( group ) class UserGroupAssociation( object ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index afdaf27c4c3..3acd38ec787 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -604,7 +604,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): #load local galaxy security policy result.security_agent = GalaxyRBACAgent( result ) # Ensure group named 'public' exists - public_group = result.Group.get_by( name='public' ) + public_group = result.Group.filter_by( name='public' ).first() if not public_group: public_group = result.security_agent.create_group( name = 'public' ) # Store public group id diff --git a/lib/galaxy/model/mapping_tests.py b/lib/galaxy/model/mapping_tests.py index e040c59e393..bfefcabddde 100644 --- a/lib/galaxy/model/mapping_tests.py +++ b/lib/galaxy/model/mapping_tests.py @@ -22,13 +22,13 @@ class MappingTests( unittest.TestCase ): model.context.current.flush() model.context.current.clear() # Check - users = model.User.select() + users = model.User.query().all() assert len( users ) == 1 assert users[0].email == "james@foo.bar.baz" assert users[0].password == "password" assert len( users[0].histories ) == 1 assert users[0].histories[0].name == "History 1" - hists = model.History.select() + hists = model.History.query().all() assert hists[0].name == "History 1" assert hists[1].name == ( "H" * 255 ) assert hists[0].user == users[0] @@ -40,7 +40,7 @@ class MappingTests( unittest.TestCase ): hists[1].name = "History 2b" model.context.current.flush() model.context.current.clear() - hists = model.History.select() + hists = model.History.query().all() assert hists[0].name == "History 1" assert hists[1].name == "History 2b" # gvk TODO need to ad test for GalaxySessions, but not yet sure what they should look like. diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 425b1600598..90aaa0ac3db 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -144,7 +144,7 @@ class GalaxyRBACAgent( RBACAgent ): return self.model.Group.get( id ) raise 'No valid method of retrieving requested group by id %s' % ( str( id ) ) def get_group_by_name( self, name ): - return self.model.Group.get_by( name=name ) + return self.model.Group.filter_by( name=name ).first() raise 'No valid method of retrieving requested group by name %s' % ( str( name ) ) def create_group( self, **kwd ): rval = self.model.Group( **kwd ) @@ -260,13 +260,13 @@ class GalaxyRBACAgent( RBACAgent ): 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 ) + return self.model.GroupDatasetAssociation.filter_by( group_id=kwd['group'].id, dataset_id=kwd['dataset'].id ).first() elif 'user' in kwd: if 'group' in kwd: - return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) + return self.model.UserGroupAssociation.filter_by( group_id=kwd['group'].id, user_id=kwd['user'].id ).first() 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 ) ) + return bool( self.model.GroupDatasetAssociation.filter_by( group_id=group_id, dataset_id=dataset_id ).first() ) def check_folder_contents( self, user, entry ): """ Return true if there are any datasets under 'folder' that the diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index c7035dd98eb..0c71bebe4d8 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -4,6 +4,7 @@ from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent import galaxy.model +from galaxy.model.orm import * from xml.sax.saxutils import escape, unescape import pkg_resources pkg_resources.require( "SQLAlchemy >= 0.4" ) @@ -138,7 +139,7 @@ class Admin( BaseController ): if not name: msg = "Please enter a name" trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) - elif len( trans.app.model.Group.select_by( name=name ) ) > 0: + elif trans.app.model.Group.filter_by( name=name ).first(): msg = "A group with that name already exists" trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) else: @@ -550,7 +551,7 @@ class Admin( BaseController ): message = kwd['message'] else: message = None - return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ), message = message ) + return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all(), message = message ) libraries = library_browser @web.expose def library( self, trans, id=None, **kwd ): diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 1d42735603d..5db40832306 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -1,5 +1,5 @@ - from galaxy.web.base.controller import * +from galaxy.model.orm import * import logging log = logging.getLogger( __name__ ) @@ -7,7 +7,7 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose def browse( self, trans, **kwd ): - return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ) ) + return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all() ) index = browse @web.expose def import_datasets( self, trans, import_ids=[], **kwd ): diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 2dab4c970bc..63bfc1aab85 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -1,15 +1,11 @@ """ Contains the main interface in the Universe class """ -from galaxy.web.base.controller import * - -import logging, os, sets, string, shutil -import re, socket - -from galaxy import util, datatypes, jobs, web, util - +import logging, os, sets, string, shutil, urllib, re, socket from cgi import escape, FieldStorage -import urllib +from galaxy import util, datatypes, jobs, web, util +from galaxy.web.base.controller import * +from galaxy.model.orm import * log = logging.getLogger( __name__ ) @@ -449,13 +445,13 @@ class RootController( BaseController ): if not email: return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) user = trans.get_user() - send_to_user = trans.app.model.User.get_by( email = email ) + send_to_user = trans.app.model.User.filter_by( email=email ).first() p = util.Params( kwd ) if p.action: if p.action == "no_share": trans.response.send_redirect( url_for( action='history_options' ) ) try: - send_to_group = trans.app.model.Group.select_by( name = send_to_user.email + ' private group' )[0] + send_to_group = trans.app.model.Group.filter_by( name=send_to_user.email+' private group' ).first() except: send_to_group = None if not send_to_group: @@ -532,7 +528,7 @@ class RootController( BaseController ): new_history.user_id = user.id galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -549,7 +545,7 @@ class RootController( BaseController ): new_history.user_id = None galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -574,7 +570,7 @@ class RootController( BaseController ): if new_history: galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 5b00894d1f9..c5c2de4a368 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -1,9 +1,8 @@ """ Contains the user interface in the Universe class """ - from galaxy.web.base.controller import * - +from galaxy.model.orm import * import logging, os, string from random import choice @@ -53,7 +52,7 @@ class User( BaseController ): email_err = "Please enter a real email address" elif len( email) > 255: email_err = "Email address exceeds maximum allowable length" - elif len( trans.app.model.User.select_by( email=email ) ) > 0: + elif trans.app.model.User.filter_by( email=email ).first(): email_err = "User with that email already exists" elif email != conf_email: conf_email_err = "Email addresses do not match." @@ -73,7 +72,7 @@ class User( BaseController ): email_error = password_error = None # Attempt login if email or password: - user = trans.app.model.User.get_by( email = email ) + user = trans.app.model.User.filter_by( email=email ).first() if not user: email_error = "No such user" elif user.external: @@ -108,7 +107,7 @@ class User( BaseController ): email_error = "Please enter a real email address" elif len( email) > 255: email_error = "Email address exceeds maximum allowable length" - elif len( trans.app.model.User.select_by( email=email ) ) > 0: + elif trans.app.model.User.filter_by( email=email ).first(): email_error = "User with that email already exists" elif len( password ) < 6: password_error = "Please use a password of at least 6 characters" @@ -144,7 +143,7 @@ class User( BaseController ): @web.expose def reset_password(self, trans, email=None, **kwd): error = '' - reset_user = trans.app.model.User.get_by( email = email ) + reset_user = trans.app.model.User.filter_by( email=email ).first() user = trans.get_user() if reset_user: if user and user.id != reset_user.id: diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 75b68bf3bf8..b0b17b92830 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -46,7 +46,7 @@ class WorkflowController( BaseController ): # Load workflow from database stored = get_stored_workflow( trans, id ) if email: - other = model.User.get_by( email=email ) + other = model.User.filter_by( email=email ).first() if not other: mtype = "error" msg = ( "User '%s' does not exist" % email ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 20bda3c2d8e..b3628b98d90 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -172,7 +172,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if secure_id: session_key = self.security.decode_session_key( secure_id ) try: - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid and galaxy_session.current_history_id: history = self.app.model.History.get( galaxy_session.current_history_id ) if history and not history.deleted: @@ -217,7 +217,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.user_id = self.user.id try: # See if we have already associated the history with the session - association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=history.id )[0] + association = self.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=history.id ).first() except: association = None history.add_galaxy_session( galaxy_session, association=association ) @@ -266,7 +266,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): """Return the user in $HTTP_REMOTE_USER and create if necessary""" # remote_user middleware ensures HTTP_REMOTE_USER exists try: - user = self.app.model.User.selectone_by( email=self.environ[ 'HTTP_REMOTE_USER' ] ) + user = self.app.model.User.filter_by( email=self.environ[ 'HTTP_REMOTE_USER' ] ).first() except: user = self.app.model.User( email=self.environ[ 'HTTP_REMOTE_USER' ] ) user.set_password_cleartext( 'external' ) @@ -283,7 +283,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if secure_id: session_key = self.security.decode_session_key( secure_id ) try: - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid and galaxy_session.user_id: user = self.app.model.User.get( galaxy_session.user_id ) if user: @@ -323,7 +323,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): session_key = self.security.decode_session_key( secure_id ) try: # Retrive the galaxy_session id via the unique session_key - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid: self.__galaxy_session = galaxy_session except: @@ -384,7 +384,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if self.history is not None: # See if we have already associated the session with the history try: - association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=self.history.id )[0] + association = self.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=self.history.id ).first() except: association = None galaxy_session.add_history( self.history, association=association ) diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index 185a18107c3..af9b0550b17 100644 --- a/lib/galaxy/webapps/reports/controllers/users.py +++ b/lib/galaxy/webapps/reports/controllers/users.py @@ -2,6 +2,7 @@ from datetime import * import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model +from galaxy.model.orm import * import pkg_resources pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa @@ -13,15 +14,7 @@ class Users( BaseController ): def registered_users( self, trans, **kwd ): params = util.Params( kwd ) msg = '' - engine = galaxy.model.mapping.metadata.engine - s = """ - SELECT - count(id) AS num_users - FROM - galaxy_user - """ - rows = engine.text( s ).execute().fetchall() - num_users = rows[0].num_users + num_users = galaxy.model.User.query().count() return trans.fill_template( 'registered_users.mako', num_users=num_users, msg=msg ) @web.expose def registered_users_per_month( self, trans, **kwd ): diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 031eef67b61..5b9063b6afa 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -59,7 +59,7 @@ class TestHistory( TwillTestCase ): # twill version 0.9 still does not allow for the following test #def test_15_add_group_member( self ): # """Testing adding a member to an existing group""" - # group = galaxy.model.Group.get_by( name='New Test Group' ) + # group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] # group_id = str( group.id ) # group_name = group.name.replace( ' ', '+' ) # self.add_group_member( group_id=group_id, group_name=group_name ) @@ -70,17 +70,17 @@ class TestHistory( TwillTestCase ): """Testing deleting a group""" self.visit_page( "admin/groups" ) self.check_page_for_string( "group_name=New+Test+Group" ) - group = galaxy.model.Group.get_by( name='New Test Group' ) + group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] group_id = str( group.id ) self.mark_group_deleted( group_id=group_id ) def test_25_undelete_group( self ): """Testing undeleting a deleted group""" - group = galaxy.model.Group.get_by( name='New Test Group' ) + group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] group_id = str( group.id ) self.undelete_group( group_id=group_id ) def test_30_purge_group( self ): """Testing purging a group""" - group = galaxy.model.Group.get_by( name='New Test Group' ) + group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] self.purge_group( group=group ) def test_20_create_library( self ): From d41deccfe1ba4a339eea7b0a939d371592d7354e Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 30 Sep 2008 09:31:46 -0400 Subject: [PATCH 65/94] Add ability for external applications to send data to Galaxy. The /root/dataset_import() method makes use of the upload tool, taking advantage of all of the safety checks incorporated into the tool. --- lib/galaxy/tools/actions/upload.py | 14 +++++++++++- lib/galaxy/web/controllers/root.py | 34 ++++++++++++++++++++++++++++- tools/data_destination/epigraph.xml | 6 ++--- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 9fba4fe467d..b33e1a1fbd8 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -36,12 +36,24 @@ class UploadToolAction( object ): return self.upload_empty( trans, "Error:", str( e ) ) if url_paste not in [ None, "" ]: if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + # If we were sent a DATA_URL from an external application in a post, NAME and INFO + # values should be in the request + if 'NAME' in incoming and incoming[ 'NAME' ] not in [ "None", None ]: + NAME = incoming[ 'NAME' ] + else: + NAME = '' + if 'INFO' in incoming and incoming[ 'INFO' ] not in [ "None", None ]: + INFO = incoming[ 'INFO' ] + else: + INFO = "uploaded url" url_paste = url_paste.replace( '\r', '' ).split( '\n' ) for line in url_paste: line = line.rstrip( '\r\n' ) if line: + if not NAME: + NAME = line try: - data_list.append( self.add_file( trans, urllib.urlopen( line ), line, file_type, dbkey, info="uploaded url", space_to_tab=space_to_tab ) ) + data_list.append( self.add_file( trans, urllib.urlopen( line ), NAME, file_type, dbkey, info=INFO, space_to_tab=space_to_tab ) ) except Exception, e: return self.upload_empty( trans, "Error:", str( e ) ) else: diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 63bfc1aab85..5f3a60874d2 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -87,7 +87,39 @@ class RootController( BaseController ): return trans.fill_template("root/history_item.mako", data=data, hid=hid) else: return trans.show_error_message( "Must specify a dataset id.") - + + @web.expose + def dataset_import( self, trans, **kwd ): + """ + External applications (e.g., EpiGRAPH) can import data to Galaxy by passing the following: + 1. DATA_URL - the url to which Galaxy should post a request to retrieve the data + 2. GENOME - the name of the UCSC genome assembly (e.g. hg18), dbkey in Galaxy + 3. NAME - data.name in Galaxy + 4. INFO - data.info in Galaxy + This method will create the tool parameters expected by the upload tool so that it + can be executed to retrieve the data from the external application. + """ + params_dict = {} + params = util.Params( kwd ) + DATA_URL = params.get( 'DATA_URL', None ) + assert DATA_URL is not None, "Required DATA_URL parameter missing from request" + params_dict[ 'url_paste' ] = DATA_URL + params_dict[ 'dbkey' ] = params.get( 'GENOME', '?' ) + NAME = params.get( 'NAME', None ) + assert NAME is not None, "Required NAME parameter missing from request" + params_dict[ 'NAME' ] = NAME + INFO = params.get( 'INFO', None ) + assert INFO is not None, "Required INFO parameter missing from request" + params_dict[ 'INFO' ] = INFO + params_dict[ 'runtool_btn' ] = 'Execute' + tool_id = 'upload1' + history = trans.get_history() + trans.ensure_valid_galaxy_session() + tool = trans.get_toolbox().tools_by_id.get( tool_id ) + template, vars = tool.handle_input( trans, params_dict ) + trans.log_event( "/root/dataset_import tool params: %s" % ( str( params_dict ) ), tool_id=tool_id ) + trans.response.send_redirect( url_for( "/index" ) ) + @web.json def history_item_updates( self, trans, ids=None, states=None ): # Avoid caching diff --git a/tools/data_destination/epigraph.xml b/tools/data_destination/epigraph.xml index 5f68e7bbc76..4ccd1378cc7 100644 --- a/tools/data_destination/epigraph.xml +++ b/tools/data_destination/epigraph.xml @@ -1,6 +1,6 @@ - - Genome analysis and prediction + + analysis and prediction with EpiGRAPH GENOME=${input1.dbkey} NAME=${input1.name} INFO=${input1.info} @@ -9,7 +9,7 @@ - + From 745cba211ab391da1c5d16c1e17460bf0ade4b7f Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 30 Sep 2008 10:39:46 -0400 Subject: [PATCH 66/94] Fixes for tool_conf.xml.sample, this file is not being properly kept current... --- tool_conf.xml.sample | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index 8b8cdca2964..addb40fa216 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -123,7 +123,6 @@ -
            @@ -156,8 +155,8 @@
            - - + +