From 199e3327df08b8ebf497936fdcd29c4fc07d01ce Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 15 Aug 2008 16:40:13 -0400 Subject: [PATCH] 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 +
+ +
+
+
+
+