def security_check(trans, item, check_ownership=False, check_accessible=False): """ Security checks for an item: checks if (a) user owns item or (b) item is accessible to user. This is a generic method for dealing with objects uniformly from the older controller mixin code - however whenever possible the managers for a particular model should be used to perform security checks. """ # all items are accessible to an admin if trans.user_is_admin: return item # Verify ownership: there is a current user and that user is the same as the item's if check_ownership: if not trans.user: raise exceptions.ItemOwnershipException("Must be logged in to manage Galaxy items", type='error') if item.user != trans.user: raise exceptions.ItemOwnershipException("%s is not owned by the current user" % item.__class__.__name__, type='error') # Verify accessible: # if it's part of a lib - can they access via security # if it's something else (sharable) have they been added to the item's users_shared_with_dot_users if check_accessible: if type(item) in (trans.app.model.LibraryFolder, trans.app.model.LibraryDatasetDatasetAssociation, trans.app.model.LibraryDataset): if not trans.app.security_agent.can_access_library_item(trans.get_current_user_roles(), item, trans.user): raise exceptions.ItemAccessibilityException("%s is not accessible to the current user" % item.__class__.__name__, type='error') else: if (item.user != trans.user) and (not item.importable) and (trans.user not in item.users_shared_with_dot_users): raise exceptions.ItemAccessibilityException("%s is not accessible to the current user" % item.__class__.__name__, type='error') return item
def __get_job(self, trans, id): try: decoded_job_id = self.decode_id(id) except Exception: raise exceptions.MalformedId() job = trans.sa_session.query(trans.app.model.Job).filter(trans.app.model.Job.id == decoded_job_id).first() if job is None: raise exceptions.ObjectNotFound() if not trans.user_is_admin() and job.user != trans.user: if not job.output_datasets: raise exceptions.ItemAccessibilityException("Job has no output datasets.") for data_assoc in job.output_datasets: if not self.dataset_manager.is_accessible(data_assoc.dataset.dataset, trans.user): raise exceptions.ItemAccessibilityException("You are not allowed to rerun this job.") return job
def check_security(self, trans, has_workflow, check_ownership=True, check_accessible=True): """ check accessibility or ownership of workflows, storedworkflows, and workflowinvocations. Throw an exception or returns True if user has needed level of access. """ if not check_ownership or check_accessible: return True # If given an invocation follow to workflow... if isinstance(has_workflow, model.WorkflowInvocation): has_workflow = has_workflow.workflow # stored workflow contains security stuff - follow that workflow to # that unless given a stored workflow. if isinstance(has_workflow, model.Workflow): stored_workflow = has_workflow.top_level_stored_workflow else: stored_workflow = has_workflow if stored_workflow.user != trans.user and not trans.user_is_admin(): if check_ownership: raise exceptions.ItemOwnershipException() # else check_accessible... if trans.sa_session.query( model.StoredWorkflowUserShareAssociation).filter_by( user=trans.user, stored_workflow=stored_workflow).count() == 0: raise exceptions.ItemAccessibilityException() return True
def try_get_authz_config(sa_session, user_id, authz_id): """ It returns a cloudauthz config (see model.CloudAuthz) with the given ID; and raise an exception if either a config with given ID does not exist, or the configuration is defined for a another user than trans.user. :type trans: galaxy.web.framework.webapp.GalaxyWebTransaction :param trans: Galaxy web transaction :type authz_id: int :param authz_id: The ID of a CloudAuthz configuration to be used for getting temporary credentials. :rtype : model.CloudAuthz :return: a cloudauthz configuration. """ qres = sa_session.query(model.CloudAuthz).get(authz_id) if qres is None: raise exceptions.ObjectNotFound( "An authorization configuration with given ID not found.") if user_id != qres.user_id: msg = "The request authorization configuration (with ID:`{}`) is not accessible for user with " \ "ID:`{}`.".format(qres.id, user_id) log.warning(msg) raise exceptions.ItemAccessibilityException(msg) return qres
def check_accessible( self, trans, history ): """ Raises error if the current user can't access the history. """ if self.is_accessible( trans, history ): return history raise exceptions.ItemAccessibilityException( "History is not accessible to the current user", type='error' )
def set_metadata(self, trans, dataset_assoc, overwrite=False, validate=True): """Trigger a job that detects and sets metadata on a given dataset association (ldda or hda)""" data = trans.sa_session.query(self.model_class).get(dataset_assoc.id) if not data.ok_to_edit_metadata(): raise exceptions.ItemAccessibilityException( 'This dataset is currently being used as input or output. You cannot edit metadata until the jobs have completed or you have canceled them.' ) else: if overwrite: for name, spec in data.metadata.spec.items(): # We need to be careful about the attributes we are resetting if name not in ['name', 'info', 'dbkey', 'base_name']: if spec.get('default'): setattr(data.metadata, name, spec.unwrap(spec.get('default'))) self.app.datatypes_registry.set_external_metadata_tool.tool_action.execute( self.app.datatypes_registry.set_external_metadata_tool, trans, incoming={ 'input1': data, 'validate': validate }, overwrite=overwrite)
def update(self, trans, library, name=None, description=None, synopsis=None): """ Update the given library """ changed = False if not trans.user_is_admin(): raise exceptions.ItemAccessibilityException( 'Only administrators can update libraries.') if library.deleted: raise exceptions.RequestParameterInvalidException( 'You cannot modify a deleted library. Undelete it first.') if name is not None: library.name = name changed = True # When library is renamed the root folder has to be renamed too. folder_manager = folders.FolderManager() folder_manager.update(trans, library.root_folder, name=name) if description is not None: library.description = description changed = True if synopsis is not None: library.synopsis = synopsis changed = True if changed: trans.sa_session.add(library) trans.sa_session.flush() return library
def __authorize_job_access(self, trans, encoded_job_id, **kwargs): for key in ["path", "job_key"]: if key not in kwargs: error_message = f"Job files action requires a valid '{key}'." raise exceptions.ObjectAttributeMissingException(error_message) job_id = trans.security.decode_id(encoded_job_id) job_key = trans.security.encode_id(job_id, kind="jobs_files") if not util.safe_str_cmp(kwargs["job_key"], job_key): raise exceptions.ItemAccessibilityException("Invalid job_key supplied.") # Verify job is active. Don't update the contents of complete jobs. job = trans.sa_session.query(model.Job).get(job_id) if job.finished: error_message = "Attempting to read or modify the files of a job that has already completed." raise exceptions.ItemAccessibilityException(error_message) return job
def create( self, trans, encoded_folder_id, payload, **kwd ): """ * POST /api/folders/{encoded_id}/contents create a new library file from an HDA :param encoded_folder_id: the encoded id of the folder to import dataset(s) to :type encoded_folder_id: an encoded id string :param payload: dictionary structure containing: :param from_hda_id: (optional) the id of an accessible HDA to copy into the library :type from_hda_id: encoded id :param ldda_message: (optional) the new message attribute of the LDDA created :type ldda_message: str :param extended_metadata: (optional) dub-dictionary containing any extended metadata to associate with the item :type extended_metadata: dict :type payload: dict :returns: a dictionary containing the id, name, and 'show' url of the new item :rtype: dict :raises: ObjectAttributeInvalidException, InsufficientPermissionsException, ItemAccessibilityException, InternalServerError """ encoded_folder_id_16 = self.__decode_library_content_id( trans, encoded_folder_id ) from_hda_id, ldda_message = ( payload.pop( 'from_hda_id', None ), payload.pop( 'ldda_message', '' ) ) if ldda_message: ldda_message = util.sanitize_html.sanitize_html( ldda_message, 'utf-8' ) rval = {} try: decoded_hda_id = self.decode_id( from_hda_id ) hda = self.hda_manager.get_owned( decoded_hda_id, trans.user, current_history=trans.history ) hda = self.hda_manager.error_if_uploading( hda ) folder = self.get_library_folder( trans, encoded_folder_id_16, check_accessible=True ) library = folder.parent_library if library.deleted: raise exceptions.ObjectAttributeInvalidException() if not self.can_current_user_add_to_library_item( trans, folder ): raise exceptions.InsufficientPermissionsException() ldda = self.copy_hda_to_library_folder( trans, hda, folder, ldda_message=ldda_message ) update_time = ldda.update_time.strftime( "%Y-%m-%d %I:%M %p" ) ldda_dict = ldda.to_dict() rval = trans.security.encode_dict_ids( ldda_dict ) rval['update_time'] = update_time except exceptions.ObjectAttributeInvalidException: raise exceptions.ObjectAttributeInvalidException( 'You cannot add datasets into deleted library. Undelete it first.' ) except exceptions.InsufficientPermissionsException: raise exceptions.exceptions.InsufficientPermissionsException( 'You do not have proper permissions to add a dataset to a folder with id (%s)' % ( encoded_folder_id ) ) except Exception as exc: # TODO handle exceptions better within the mixins if ( ( 'not accessible to the current user' in str( exc ) ) or ( 'You are not allowed to access this dataset' in str( exc ) ) ): raise exceptions.ItemAccessibilityException( 'You do not have access to the requested item' ) else: log.exception( exc ) raise exceptions.InternalServerError( 'An unknown error ocurred. Please try again.' ) return rval
def error_unless_accessible(self, item, user, **kwargs): """ Raise an error if the item is NOT accessible to user, otherwise return the item. :raises exceptions.ItemAccessibilityException: """ if self.is_accessible(item, user, **kwargs): return item raise exceptions.ItemAccessibilityException(f"{self.model_class.__name__} is not accessible by user")
def __authorize_job_access(self, encoded_job_id, **kwargs): key = "job_key" if key not in kwargs: error_message = "Job files action requires a valid '%s'." % key raise exceptions.ObjectAttributeMissingException(error_message) job_id = self._security.decode_id(encoded_job_id) job_key = self._security.encode_id(job_id, kind="jobs_files") if not util.safe_str_cmp(kwargs["job_key"], job_key): raise exceptions.ItemAccessibilityException("Invalid job_key supplied.") # Verify job is active. Don't update the contents of complete jobs. sa_session = self._app.model.context.current job = sa_session.query(model.Job).get(job_id) if not job.running: error_message = "Attempting to read or modify the files of a job that has already completed." raise exceptions.ItemAccessibilityException(error_message) return job
def __get_stored_accessible_workflow( self, trans, workflow_id ): stored_workflow = self.__get_stored_workflow( trans, workflow_id ) # check to see if user has permissions to selected workflow if stored_workflow.user != trans.user and not trans.user_is_admin(): if trans.sa_session.query(trans.app.model.StoredWorkflowUserShareAssociation).filter_by(user=trans.user, stored_workflow=stored_workflow).count() == 0: message = "Workflow is not owned by or shared with current user" raise exceptions.ItemAccessibilityException( message ) return stored_workflow
def access_entry_point_target(self, trans, entry_point_id): entry_point = trans.sa_session.query(model.InteractiveToolEntryPoint).get(entry_point_id) if self.app.interactivetool_manager.can_access_entry_point(trans, entry_point): if entry_point.active: return self.target_if_active(trans, entry_point) elif entry_point.deleted: raise exceptions.MessageException('InteractiveTool has ended. You will have to start a new one.') else: raise exceptions.MessageException('InteractiveTool is not active. If you recently launched this tool it may not be ready yet, please wait a moment and refresh this page.') raise exceptions.ItemAccessibilityException("You do not have access to this InteractiveTool entry point.")
def error_unless_accessible(self, trans, item, user): """ Raise an error if the item is NOT accessible to user, otherwise return the item. :raises exceptions.ItemAccessibilityException: """ if self.is_accessible(trans, item, user): return item raise exceptions.ItemAccessibilityException( "%s is not accessible by user" % (self.model_class.__name__))
def create(self, trans, encoded_folder_id, payload, **kwd): """ * POST /api/folders/{encoded_id}/contents create a new library file from an HDA :param encoded_folder_id: the encoded id of the folder to import dataset(s) to :type encoded_folder_id: an encoded id string :param payload: dictionary structure containing: :param from_hda_id: (optional) the id of an accessible HDA to copy into the library :type from_hda_id: encoded id :param from_hdca_id: (optional) the id of an accessible HDCA to copy into the library :type from_hdca_id: encoded id :param ldda_message: (optional) the new message attribute of the LDDA created :type ldda_message: str :param extended_metadata: (optional) dub-dictionary containing any extended metadata to associate with the item :type extended_metadata: dict :type payload: dict :returns: a dictionary describing the new item if ``from_hda_id`` is supplied or a list of such dictionaries describing the new items if ``from_hdca_id`` is supplied. :rtype: object :raises: ObjectAttributeInvalidException, InsufficientPermissionsException, ItemAccessibilityException, InternalServerError """ encoded_folder_id_16 = self.__decode_library_content_id( trans, encoded_folder_id) from_hda_id = payload.pop('from_hda_id', None) from_hdca_id = payload.pop('from_hdca_id', None) ldda_message = payload.pop('ldda_message', '') if ldda_message: ldda_message = util.sanitize_html.sanitize_html( ldda_message, 'utf-8') try: if from_hda_id: decoded_hda_id = self.decode_id(from_hda_id) return self._copy_hda_to_library_folder( trans, self.hda_manager, decoded_hda_id, encoded_folder_id_16, ldda_message) if from_hdca_id: decoded_hdca_id = self.decode_id(from_hdca_id) return self._copy_hdca_to_library_folder( trans, self.hda_manager, decoded_hdca_id, encoded_folder_id_16, ldda_message) except Exception as exc: # TODO handle exceptions better within the mixins if 'not accessible to the current user' in str( exc ) or 'You are not allowed to access this dataset' in str(exc): raise exceptions.ItemAccessibilityException( 'You do not have access to the requested item') else: log.exception(exc) raise exc
def can_user_assume_authn(trans, authn_id): qres = trans.sa_session.query(model.UserAuthnzToken).get(authn_id) if qres is None: msg = "Authentication record with the given `authn_id` (`{}`) not found.".format( trans.security.encode_id(authn_id)) log.debug(msg) raise exceptions.ObjectNotFound(msg) if qres.user_id != trans.user.id: msg = "The request authentication with ID `{}` is not accessible to user with ID " \ "`{}`.".format(trans.security.encode_id(authn_id), trans.security.encode_id(trans.user.id)) log.warning(msg) raise exceptions.ItemAccessibilityException(msg)
def show(self, trans, id, **kwd): """ GET /api/workflows/{encoded_workflow_id} Displays information needed to run a workflow from the command line. """ stored_workflow = self.__get_stored_workflow( trans, id ) if stored_workflow.importable is False and stored_workflow.user != trans.user and not trans.user_is_admin(): if trans.sa_session.query(trans.app.model.StoredWorkflowUserShareAssociation).filter_by(user=trans.user, stored_workflow=stored_workflow).count() == 0: message = "Workflow is neither importable, nor owned by or shared with current user" raise exceptions.ItemAccessibilityException( message ) return self.workflow_contents_manager.workflow_to_dict( trans, stored_workflow, style="instance" )
def delete(self, trans, id, **kwd): """ delete( self, trans, id, **kwd ) * DELETE /api/libraries/{id} marks the library with the given ``id`` as `deleted` (or removes the `deleted` mark if the `undelete` param is true) .. note:: Currently, only admin users can un/delete libraries. :param id: the encoded id of the library to un/delete :type id: an encoded id string :param undelete: (optional) flag specifying whether the item should be deleted or undeleted, defaults to false: :type undelete: bool :returns: detailed library information :rtype: dictionary .. seealso:: :attr:`galaxy.model.Library.dict_element_visible_keys` :raises: ItemAccessibilityException, MalformedId, ObjectNotFound """ undelete = util.string_as_bool(kwd.get('undelete', False)) if not trans.user_is_admin(): raise exceptions.ItemAccessibilityException( 'Only administrators can delete and undelete libraries.') try: decoded_id = trans.security.decode_id(id) except Exception: raise exceptions.MalformedId( 'Malformed library id ( %s ) specified, unable to decode.' % id) try: library = trans.sa_session.query( trans.app.model.Library).get(decoded_id) except Exception: library = None if not library: raise exceptions.ObjectNotFound( 'Library with the id provided ( %s ) was not found' % id) if undelete: library.deleted = False else: library.deleted = True trans.sa_session.add(library) trans.sa_session.flush() return library.to_dict(view='element', value_mapper={ 'id': trans.security.encode_id, 'root_folder_id': trans.security.encode_id })
def create(self, trans, name, description='', synopsis=''): """ Create a new library. """ if not trans.user_is_admin(): raise exceptions.ItemAccessibilityException('Only administrators can create libraries.') else: library = trans.app.model.Library(name=name, description=description, synopsis=synopsis) root_folder = trans.app.model.LibraryFolder(name=name, description='') library.root_folder = root_folder trans.sa_session.add_all((library, root_folder)) trans.sa_session.flush() return library
def delete(self, trans, library, undelete=False): """ Mark given library deleted/undeleted based on the flag. """ if not trans.user_is_admin(): raise exceptions.ItemAccessibilityException('Only administrators can delete and undelete libraries.') if undelete: library.deleted = False else: library.deleted = True trans.sa_session.add(library) trans.sa_session.flush() return library
def __check_job_can_write_to_path(self, trans, job, path): """ Verify an idealized job runner should actually be able to write to the specified path - it must be a dataset output, a dataset "extra file", or a some place in the working directory of this job. Would like similar checks for reading the unstructured nature of loc files make this very difficult. (See abandoned work here https://gist.github.com/jmchilton/9103619.) """ in_work_dir = self.__in_working_directory(job, path, trans.app) allow_temp_dir_file = self.__is_allowed_temp_dir_file(trans.app, job, path) if not in_work_dir and not allow_temp_dir_file and not self.__is_output_dataset_path(job, path): raise exceptions.ItemAccessibilityException("Job is not authorized to write to supplied path.")
def get_stored_accessible_workflow(self, trans, workflow_id): """ Get a stored workflow from a encoded stored workflow id and make sure it accessible to the user. """ stored_workflow = self.get_stored_workflow(trans, workflow_id) # check to see if user has permissions to selected workflow if stored_workflow.user != trans.user and not trans.user_is_admin() and not stored_workflow.published: if trans.sa_session.query(trans.app.model.StoredWorkflowUserShareAssociation).filter_by(user=trans.user, stored_workflow=stored_workflow).count() == 0: message = "Workflow is not owned by or shared with current user" raise exceptions.ItemAccessibilityException(message) return stored_workflow
def set_metadata(self, trans, dataset_assoc): """Trigger a job that detects and sets metadata on a given dataset association (ldda or hda)""" data = trans.sa_session.query(self.model_class).get(dataset_assoc.id) if not self.__ok_to_edit_metadata(trans, data.id): raise exceptions.ItemAccessibilityException( 'This dataset is currently being used as input or output. You cannot edit metadata until the jobs have completed or you have canceled them.' ) else: trans.app.datatypes_registry.set_external_metadata_tool.tool_action.execute( trans.app.datatypes_registry.set_external_metadata_tool, trans, incoming={'input1': data}, overwrite=False) # overwrite is False as per existing behavior
def __api_import_shared_workflow(self, trans, workflow_id, payload, **kwd): try: stored_workflow = self.get_stored_workflow(trans, workflow_id, check_ownership=False) except Exception: raise exceptions.ObjectNotFound("Malformed workflow id ( %s ) specified." % workflow_id) if stored_workflow.importable is False: raise exceptions.ItemAccessibilityException('The owner of this workflow has disabled imports via this link.') elif stored_workflow.deleted: raise exceptions.ItemDeletionException("You can't import this workflow because it has been deleted.") imported_workflow = self._import_shared_workflow(trans, stored_workflow) item = imported_workflow.to_dict(value_mapper={'id': trans.security.encode_id}) encoded_id = trans.security.encode_id(imported_workflow.id) item['url'] = url_for('workflow', id=encoded_id) return item
def check_accessible(self, trans, hda): """ Raise error if HDA is not accessible. """ if trans.user and trans.user_is_admin(): return hda # check for access of the containing history... self.histories_mgr.check_accessible(trans, hda.history) # ...then the underlying dataset if self.can_access_dataset(trans, hda): return hda raise exceptions.ItemAccessibilityException( "HistoryDatasetAssociation is not accessible to the current user", type='error')
def create(self, trans, payload, **kwd): """ create( self, trans, payload, **kwd ) * POST /api/libraries: Creates a new library. Only ``name`` parameter is required. .. note:: Currently, only admin users can create libraries. :param payload: dictionary structure containing:: 'name': the new library's name (required) 'description': the new library's description (optional) 'synopsis': the new library's synopsis (optional) :type payload: dict :returns: detailed library information :rtype: dict :raises: ItemAccessibilityException, RequestParameterMissingException """ if not trans.user_is_admin(): raise exceptions.ItemAccessibilityException( 'Only administrators can create libraries.') params = util.Params(payload) name = util.restore_text(params.get('name', None)) if not name: raise exceptions.RequestParameterMissingException( "Missing required parameter 'name'.") description = util.restore_text(params.get('description', '')) synopsis = util.restore_text(params.get('synopsis', '')) if synopsis in ['None', None]: synopsis = '' library = trans.app.model.Library(name=name, description=description, synopsis=synopsis) root_folder = trans.app.model.LibraryFolder(name=name, description='') library.root_folder = root_folder trans.sa_session.add_all((library, root_folder)) trans.sa_session.flush() item = library.to_dict(view='element', value_mapper={ 'id': trans.security.encode_id, 'root_folder_id': trans.security.encode_id }) item['can_user_add'] = True item['can_user_modify'] = True item['can_user_manage'] = True if trans.app.security_agent.library_is_public(library, contents=False): item['public'] = True return item
def __get_job(self, trans, id): try: decoded_job_id = self.decode_id(id) except Exception: raise exceptions.MalformedId() job = trans.sa_session.query(trans.app.model.Job).filter( trans.app.model.Job.id == decoded_job_id).first() if job is None: raise exceptions.ObjectNotFound() belongs_to_user = (job.user == trans.user) if job.user else ( job.session_id == trans.get_galaxy_session().id) if not trans.user_is_admin and not belongs_to_user: # Check access granted via output datasets. if not job.output_datasets: raise exceptions.ItemAccessibilityException( "Job has no output datasets.") for data_assoc in job.output_datasets: if not self.dataset_manager.is_accessible( data_assoc.dataset.dataset, trans.user): raise exceptions.ItemAccessibilityException( "You are not allowed to rerun this job.") trans.sa_session.refresh(job) return job
def detect_datatype(self, trans, dataset_assoc): """Sniff and assign the datatype to a given dataset association (ldda or hda)""" data = trans.sa_session.query(self.model_class).get(dataset_assoc.id) if data.datatype.is_datatype_change_allowed(): if not data.ok_to_edit_metadata(): raise exceptions.ItemAccessibilityException('This dataset is currently being used as input or output. You cannot change datatype until the jobs have completed or you have canceled them.') else: path = data.dataset.file_name is_binary = check_binary(path) datatype = sniff.guess_ext(path, trans.app.datatypes_registry.sniff_order, is_binary=is_binary) trans.app.datatypes_registry.change_datatype(data, datatype) trans.sa_session.flush() self.set_metadata(trans, dataset_assoc) else: raise exceptions.InsufficientPermissionsException(f'Changing datatype "{data.extension}" is not allowed.')
def index(self, trans: ProvidesUserContext, running=False, job_id=None, **kwd): """ * GET /api/entry_points Returns tool entry point information. Currently passing a job_id parameter is required, as this becomes more general that won't be needed. :type job_id: string :param job_id: Encoded job id :type running: boolean :param running: filter to only include running job entry points. :rtype: list :returns: list of entry point dictionaries. """ running = util.asbool(running) if job_id is None and not running: raise exceptions.RequestParameterInvalidException( "Currently this API must passed a job id or running=true") if job_id is not None and running: raise exceptions.RequestParameterInvalidException( "Currently this API must passed only a job id or running=true") if job_id is not None: job = trans.sa_session.query(Job).get(self.decode_id(job_id)) if not self.interactivetool_manager.can_access_job(trans, job): raise exceptions.ItemAccessibilityException() entry_points = job.interactivetool_entry_points if running: entry_points = self.interactivetool_manager.get_nonterminal_for_user_by_trans( trans) rval = [] for entry_point in entry_points: as_dict = self.encode_all_ids(trans, entry_point.to_dict(), True) target = self.interactivetool_manager.target_if_active( trans, entry_point) if target: as_dict["target"] = target rval.append(as_dict) return rval
def stop_entry_point(self, trans: ProvidesUserContext, id, **kwds): """ DELETE /api/entry_points/{id} """ if not id: raise exceptions.RequestParameterMissingException( "Must supply entry point id") try: entry_point_id = self.decode_id(id) entry_point = trans.sa_session.query( InteractiveToolEntryPoint).get(entry_point_id) except Exception: raise exceptions.RequestParameterInvalidException( "entry point '{id}' invalid") if self.app.interactivetool_manager.can_access_entry_point( trans, entry_point): self.app.interactivetool_manager.stop(trans, entry_point) else: raise exceptions.ItemAccessibilityException( f"entry point '{id}' is not accessible")