mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-25 00:12:08 +08:00
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.
This commit is contained in:
@@ -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"
|
||||
|
||||
+466
-105
@@ -27,19 +27,97 @@ 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()
|
||||
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()
|
||||
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 +179,337 @@ 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, validation_errors=None, visible=True, create_dataset = False ):
|
||||
parent_id=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 +525,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
|
||||
@@ -130,10 +535,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 ):
|
||||
@@ -251,7 +652,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 )
|
||||
des.flush()
|
||||
if copy_children:
|
||||
@@ -273,10 +675,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
|
||||
@@ -287,6 +691,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
|
||||
@@ -325,18 +741,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 ):
|
||||
@@ -347,98 +800,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
|
||||
|
||||
|
||||
@@ -113,6 +113,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 ),
|
||||
@@ -293,6 +386,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 ) ) )
|
||||
|
||||
@@ -406,6 +549,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():
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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( '<param name="blah" type="data" format="interval"/>' ) )
|
||||
>>> print p.name
|
||||
blah
|
||||
>>> print p.get_html( trans=Bunch( history=hist ) )
|
||||
>>> print p.get_html( trans=Bunch( history=hist, user=None ) )
|
||||
<select name="blah">
|
||||
<option value="2">2: Unnamed dataset</option>
|
||||
<option value="5" selected>5: Unnamed dataset</option>
|
||||
@@ -1047,7 +1053,7 @@ class DataToolParameter( ToolParameter ):
|
||||
hid = "%s.%d" % ( parent_hid, i + 1 )
|
||||
else:
|
||||
hid = str( data.hid )
|
||||
if not data.deleted and data.state not in [data.states.ERROR] and data.visible:
|
||||
if not data.deleted and data.state not in [data.states.ERROR] and data.visible and data.allow_action( trans.user, data.access_actions.USE ):
|
||||
if self.options and data.get_dbkey() != filter_value:
|
||||
continue
|
||||
if isinstance( data.datatype, self.formats):
|
||||
@@ -1061,6 +1067,8 @@ class DataToolParameter( ToolParameter ):
|
||||
data = datasets[0]
|
||||
elif not self.converter_safe( other_values, trans ):
|
||||
continue
|
||||
if not data.allow_action( trans.user, data.access_actions.USE ):
|
||||
continue
|
||||
selected = ( value and ( data in value ) )
|
||||
field.add_option( "%s: (as %s) %s" % ( hid, target_ext, data.name[:30] ), data.id, selected )
|
||||
break #we only report the first valid converter, assume self.extensions is a priority list
|
||||
|
||||
@@ -103,7 +103,7 @@ class ASync( BaseController ):
|
||||
#data.state = jobs.JOB_OK
|
||||
#history.datasets.add_dataset( data )
|
||||
|
||||
data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE )
|
||||
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.name = GALAXY_NAME
|
||||
data.dbkey = GALAXY_BUILD
|
||||
data.info = GALAXY_INFO
|
||||
|
||||
@@ -103,28 +103,27 @@ class DatasetInterface( BaseController ):
|
||||
@web.expose
|
||||
def display(self, trans, dataset_id=None, filename=None, **kwd):
|
||||
"""Catches the dataset id and displays file contents as directed"""
|
||||
if filename is None or filename.lower() == "index":
|
||||
try:
|
||||
data = trans.app.model.HistoryDatasetAssociation.get( dataset_id )
|
||||
if data:
|
||||
mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() )
|
||||
trans.response.set_content_type(mime)
|
||||
trans.log_event( "Display dataset id: %s" % str(dataset_id) )
|
||||
try:
|
||||
return open( data.file_name )
|
||||
except:
|
||||
return "This item contains no content"
|
||||
except:
|
||||
pass
|
||||
return "Invalid dataset specified"
|
||||
else:
|
||||
#display files from directory here
|
||||
try:
|
||||
file_path = os.path.join(trans.app.model.HistoryDatasetAssociation.get( dataset_id ).extra_files_path, filename)
|
||||
mime, encoding = mimetypes.guess_type(file_path)
|
||||
if mime is None:
|
||||
mime = trans.app.datatypes_registry.get_mimetype_by_extension(".".split(file_path)[-1])
|
||||
data = trans.app.model.HistoryDatasetAssociation.get( dataset_id )
|
||||
if not data:
|
||||
raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." )
|
||||
if data.allow_action( trans.user, data.access_actions.VIEW ):
|
||||
if filename is None or filename.lower() == "index":
|
||||
mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() )
|
||||
trans.response.set_content_type(mime)
|
||||
return open(file_path)
|
||||
except:
|
||||
raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % (filename) )
|
||||
trans.log_event( "Display dataset id: %s" % str( dataset_id ) )
|
||||
try:
|
||||
return open( data.file_name )
|
||||
except:
|
||||
raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) )
|
||||
else:
|
||||
file_path = os.path.join( data.extra_files_path, filename )
|
||||
mime, encoding = mimetypes.guess_type( file_path )
|
||||
if mime is None:
|
||||
mime = trans.app.datatypes_registry.get_mimetype_by_extension( ".".split( file_path )[-1] )
|
||||
trans.response.set_content_type( mime )
|
||||
try:
|
||||
return open( file_path )
|
||||
except:
|
||||
raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) )
|
||||
else:
|
||||
raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." )
|
||||
@@ -137,22 +137,25 @@ class RootController( BaseController ):
|
||||
except:
|
||||
return "Dataset id '%s' is invalid" %str( id )
|
||||
if data:
|
||||
mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() )
|
||||
trans.response.set_content_type(mime)
|
||||
if tofile:
|
||||
fStat = os.stat(data.file_name)
|
||||
trans.response.headers['Content-Length'] = int(fStat.st_size)
|
||||
if toext[0:1] != ".":
|
||||
toext = "." + toext
|
||||
valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
fname = data.name
|
||||
fname = ''.join(c in valid_chars and c or '_' for c in fname)[0:150]
|
||||
trans.response.headers["Content-Disposition"] = "attachment; filename=GalaxyHistoryItem-%s-[%s]%s" % (data.hid, fname, toext)
|
||||
trans.log_event( "Display dataset id: %s" % str(id) )
|
||||
try:
|
||||
return open( data.file_name )
|
||||
except:
|
||||
return "This dataset contains no content"
|
||||
if data.allow_action( trans.user, data.access_actions.VIEW ):
|
||||
mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() )
|
||||
trans.response.set_content_type(mime)
|
||||
if tofile:
|
||||
fStat = os.stat(data.file_name)
|
||||
trans.response.headers['Content-Length'] = int(fStat.st_size)
|
||||
if toext[0:1] != ".":
|
||||
toext = "." + toext
|
||||
valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
fname = data.name
|
||||
fname = ''.join(c in valid_chars and c or '_' for c in fname)[0:150]
|
||||
trans.response.headers["Content-Disposition"] = "attachment; filename=GalaxyHistoryItem-%s-[%s]%s" % (data.hid, fname, toext)
|
||||
trans.log_event( "Display dataset id: %s" % str(id) )
|
||||
try:
|
||||
return open( data.file_name )
|
||||
except:
|
||||
return "This dataset contains no content"
|
||||
else:
|
||||
return "You are not privileged to view this dataset."
|
||||
else:
|
||||
return "No dataset with id '%s'" % str( id )
|
||||
|
||||
@@ -164,9 +167,12 @@ class RootController( BaseController ):
|
||||
try:
|
||||
data = self.app.model.HistoryDatasetAssociation.get( parent_id )
|
||||
if data:
|
||||
child = data.get_child_by_designation(designation)
|
||||
child = data.get_child_by_designation( designation )
|
||||
if child:
|
||||
return self.display(trans, id=child.id, tofile=tofile, toext=toext)
|
||||
if child.allow_action( trans.user, child.access_actions.VIEW ):
|
||||
return self.display( trans, id=child.id, tofile=tofile, toext=toext )
|
||||
else:
|
||||
return "You are not privileged to access this dataset."
|
||||
except Exception:
|
||||
pass
|
||||
return "A child named %s could not be found for data %s" % ( designation, parent_id )
|
||||
@@ -176,9 +182,12 @@ class RootController( BaseController ):
|
||||
"""Returns a file in a format that can successfully be displayed in display_app"""
|
||||
data = self.app.model.HistoryDatasetAssociation.get( id )
|
||||
if data:
|
||||
trans.response.set_content_type(data.get_mime())
|
||||
trans.log_event( "Formatted dataset id %s for display at %s" % ( str(id), display_app ) )
|
||||
return data.as_display_type(display_app, **kwd)
|
||||
if data.allow_action( trans.user, data.access_actions.VIEW ):
|
||||
trans.response.set_content_type( data.get_mime() )
|
||||
trans.log_event( "Formatted dataset id %s for display at %s" % ( str( id ), display_app ) )
|
||||
return data.as_display_type( display_app, **kwd )
|
||||
else:
|
||||
return "You are not privileged to access this dataset."
|
||||
else:
|
||||
return "No data with id=%d" % id
|
||||
|
||||
@@ -206,69 +215,99 @@ class RootController( BaseController ):
|
||||
data = self.app.model.HistoryDatasetAssociation.get( id )
|
||||
if data is None:
|
||||
return trans.show_error_message( "Problem retrieving dataset id %s with history id %s." % ( str( id ), str( hid ) ) )
|
||||
|
||||
p = util.Params(kwd, safe=False)
|
||||
|
||||
if p.change:
|
||||
# The user clicked the Save button on the 'Change data type' form
|
||||
trans.app.datatypes_registry.change_datatype( data, p.datatype )
|
||||
trans.app.model.flush()
|
||||
elif p.save:
|
||||
# The user clicked the Save button on the 'Edit Attributes' form
|
||||
data.name = p.name
|
||||
data.info = p.info
|
||||
if data.history.user is not None and data.history.user != trans.user:
|
||||
return trans.show_error_message( "This instance of a dataset (%s) in a history does not belong to you." % ( data.id ) )
|
||||
if data.allow_action( trans.user, data.access_actions.USE ):
|
||||
p = util.Params(kwd, safe=False)
|
||||
|
||||
# The following for loop will save all metadata_spec items
|
||||
for name, spec in data.datatype.metadata_spec.items():
|
||||
if spec.get("readonly"):
|
||||
continue
|
||||
optional = p.get("is_"+name, None)
|
||||
if optional and optional == 'true':
|
||||
# optional element... == 'true' actually means it is NOT checked (and therefore ommitted)
|
||||
setattr(data.metadata,name,None)
|
||||
else:
|
||||
setattr(data.metadata,name,spec.unwrap(p.get(name, None), p))
|
||||
if p.change:
|
||||
# The user clicked the Save button on the 'Change data type' form
|
||||
trans.app.datatypes_registry.change_datatype( data, p.datatype )
|
||||
trans.app.model.flush()
|
||||
elif p.save:
|
||||
# The user clicked the Save button on the 'Edit Attributes' form
|
||||
data.name = p.name
|
||||
data.info = p.info
|
||||
|
||||
# The following for loop will save all metadata_spec items
|
||||
for name, spec in data.datatype.metadata_spec.items():
|
||||
if spec.get("readonly"):
|
||||
continue
|
||||
optional = p.get("is_"+name, None)
|
||||
if optional and optional == 'true':
|
||||
# optional element... == 'true' actually means it is NOT checked (and therefore ommitted)
|
||||
setattr(data.metadata,name,None)
|
||||
else:
|
||||
setattr(data.metadata,name,spec.unwrap(p.get(name, None), p))
|
||||
|
||||
data.datatype.after_edit( data )
|
||||
trans.app.model.flush()
|
||||
return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] )
|
||||
elif p.detect:
|
||||
# The user clicked the Auto-detect button on the 'Edit Attributes' form
|
||||
data.datatype.after_edit( data )
|
||||
trans.app.model.flush()
|
||||
return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] )
|
||||
elif p.detect:
|
||||
# The user clicked the Auto-detect button on the 'Edit Attributes' form
|
||||
for name, spec in data.datatype.metadata_spec.items():
|
||||
# We need to be careful about the attributes we are resetting
|
||||
if name != 'name' and name != 'info' and name != 'dbkey':
|
||||
if spec.get( 'default' ):
|
||||
setattr( data.metadata,name,spec.unwrap( spec.get( 'default' ), spec ))
|
||||
data.datatype.set_meta( data )
|
||||
data.datatype.after_edit( data )
|
||||
trans.app.model.flush()
|
||||
return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] )
|
||||
elif p.convert_data:
|
||||
"""The user clicked the Convert button on the 'Convert to new format' form"""
|
||||
target_type = kwd.get("target_type", None)
|
||||
if target_type:
|
||||
msg = data.datatype.convert_dataset(trans, data, target_type)
|
||||
return trans.show_ok_message( msg, refresh_frames=['history'] )
|
||||
elif p.change_permision:
|
||||
"""The user clicked the change_permision 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 permissions." )
|
||||
private_dataset = 'private_dataset'
|
||||
public_group = trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_id )
|
||||
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 ):
|
||||
for assoc in data.dataset.groups:
|
||||
if assoc.group_id == public_group.id:
|
||||
assoc.delete()
|
||||
assoc.flush()
|
||||
else:
|
||||
return trans.show_error_message( "You are not authorized to change this dataset's permissions." )
|
||||
elif private_dataset not in kwd and not data.dataset.has_group( public_group ):
|
||||
#check user has permision and then add public group
|
||||
if data.dataset.allow_action( trans.user, data.dataset.access_actions.ADD_GROUP ):
|
||||
public_group.add_dataset( data.dataset )
|
||||
public_group.flush()
|
||||
else:
|
||||
return trans.show_error_message( "You are not authorized to change this dataset's permissions." )
|
||||
else:
|
||||
return trans.show_error_message( "You have not specified a valid change of permissions." )
|
||||
return trans.show_ok_message( 'Permissions have been changed.', refresh_frames=['history'] )
|
||||
|
||||
data.datatype.before_edit( data )
|
||||
|
||||
if "dbkey" in data.datatype.metadata_spec and not data.metadata.dbkey:
|
||||
# Copy dbkey into metadata, for backwards compatability
|
||||
# This looks like it does nothing, but getting the dbkey
|
||||
# returns the metadata dbkey unless it is None, in which
|
||||
# case it resorts to the old dbkey. Setting the dbkey
|
||||
# sets it properly in the metadata
|
||||
data.metadata.dbkey = data.dbkey
|
||||
metadata = list()
|
||||
# a list of MetadataParemeters
|
||||
for name, spec in data.datatype.metadata_spec.items():
|
||||
# We need to be careful about the attributes we are resetting
|
||||
if name != 'name' and name != 'info' and name != 'dbkey':
|
||||
if spec.get( 'default' ):
|
||||
setattr( data.metadata,name,spec.unwrap( spec.get( 'default' ), spec ))
|
||||
data.datatype.set_meta( data )
|
||||
data.datatype.after_edit( data )
|
||||
trans.app.model.flush()
|
||||
return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] )
|
||||
elif p.convert_data:
|
||||
"""The user clicked the Convert button on the 'Convert to new format' form"""
|
||||
target_type = kwd.get("target_type", None)
|
||||
if target_type:
|
||||
msg = data.datatype.convert_dataset(trans, data, target_type)
|
||||
return trans.show_ok_message( msg, refresh_frames=['history'] )
|
||||
data.datatype.before_edit( data )
|
||||
|
||||
if "dbkey" in data.datatype.metadata_spec and not data.metadata.dbkey:
|
||||
# Copy dbkey into metadata, for backwards compatability
|
||||
# This looks like it does nothing, but getting the dbkey
|
||||
# returns the metadata dbkey unless it is None, in which
|
||||
# case it resorts to the old dbkey. Setting the dbkey
|
||||
# sets it properly in the metadata
|
||||
data.metadata.dbkey = data.dbkey
|
||||
metadata = list()
|
||||
# a list of MetadataParemeters
|
||||
for name, spec in data.datatype.metadata_spec.items():
|
||||
if spec.visible:
|
||||
metadata.append( spec.wrap( data.metadata.get(name), data ) )
|
||||
# let's not overwrite the imported datatypes module with the variable datatypes?
|
||||
ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()]
|
||||
ldatatypes.sort()
|
||||
trans.log_event( "Opened edit view on dataset %s" % str(id) )
|
||||
return trans.fill_template( "/dataset/edit_attributes.mako", data=data, metadata=metadata,
|
||||
datatypes=ldatatypes, err=None )
|
||||
if spec.visible:
|
||||
metadata.append( spec.wrap( data.metadata.get(name), data ) )
|
||||
# let's not overwrite the imported datatypes module with the variable datatypes?
|
||||
ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()]
|
||||
ldatatypes.sort()
|
||||
trans.log_event( "Opened edit view on dataset %s" % str(id) )
|
||||
return trans.fill_template( "/dataset/edit_attributes.mako", data=data, metadata=metadata,
|
||||
datatypes=ldatatypes, err=None )
|
||||
else:
|
||||
return trans.show_error_message( "You do not have permission to edit this dataset's (%s) attributes." % id )
|
||||
|
||||
@web.expose
|
||||
def delete( self, trans, id = None, **kwd):
|
||||
@@ -327,6 +366,8 @@ class RootController( BaseController ):
|
||||
self.app.job_stop_queue.put( data.creating_job_associations[0].job )
|
||||
except IndexError:
|
||||
pass # upload tool will cause this since it doesn't have a job
|
||||
else:
|
||||
return "Dataset id '%s' is invalid" %str( id )
|
||||
return "OK"
|
||||
|
||||
## ---- History management -----------------------------------------------
|
||||
@@ -404,7 +445,7 @@ class RootController( BaseController ):
|
||||
send_to_err = "You can't send histories to yourself"
|
||||
else:
|
||||
for history in histories:
|
||||
new_history = history.copy()
|
||||
new_history = history.copy( target_user=send_to_user )
|
||||
new_history.name = history.name+" from "+user.email
|
||||
new_history.user_id = send_to_user.id
|
||||
trans.log_event( "History share, id: %s, name: '%s': to new id: %s" % (str(history.id), history.name, str(new_history.id)) )
|
||||
@@ -441,7 +482,7 @@ class RootController( BaseController ):
|
||||
if user:
|
||||
if import_history.user_id == user.id:
|
||||
return trans.show_error_message( "You cannot import your own history.")
|
||||
new_history = import_history.copy()
|
||||
new_history = import_history.copy( target_user=trans.user )
|
||||
new_history.name = "imported: "+new_history.name
|
||||
new_history.user_id = user.id
|
||||
galaxy_session = trans.get_galaxy_session()
|
||||
@@ -548,11 +589,17 @@ class RootController( BaseController ):
|
||||
return trans.show_message( "<p>%s" % change_msg, refresh_frames=['history'] )
|
||||
|
||||
@web.expose
|
||||
def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",**kwd ):
|
||||
def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",copy_access_from=None,**kwd ):
|
||||
"""Adds a POSTed file to a History"""
|
||||
try:
|
||||
history = trans.app.model.History.get( history_id )
|
||||
data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True )
|
||||
roles = history.default_roles
|
||||
groups = history.default_groups
|
||||
if copy_access_from:
|
||||
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.flush()
|
||||
data_file = open( data.file_name, "wb" )
|
||||
file_data.file.seek( 0 )
|
||||
@@ -569,7 +616,8 @@ class RootController( BaseController ):
|
||||
data.flush()
|
||||
trans.log_event("Added dataset %d to history %d" %(data.id, trans.history.id))
|
||||
return trans.show_ok_message("Dataset "+str(data.hid)+" added to history "+str(history_id)+".")
|
||||
except:
|
||||
except Exception, e:
|
||||
trans.log_event( "Failed to add dataset to history: %s" % ( e ) )
|
||||
return trans.show_error_message("Adding File to History has Failed")
|
||||
|
||||
@web.expose
|
||||
|
||||
@@ -197,7 +197,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ):
|
||||
return self.new_history()
|
||||
return self.__history
|
||||
def new_history( self ):
|
||||
history = self.app.model.History()
|
||||
history = self.app.model.History( user = self.user )
|
||||
# Make sure we have an id
|
||||
history.flush()
|
||||
# Immediately associate the new history with self
|
||||
@@ -431,6 +431,9 @@ class UniverseWebTransaction( base.DefaultWebTransaction ):
|
||||
galaxy_session.flush()
|
||||
self.__galaxy_session = galaxy_session
|
||||
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.user_id = user.id
|
||||
history.flush()
|
||||
self.__history = history
|
||||
|
||||
Reference in New Issue
Block a user