commit/galaxy-central: carlfeberhard: History/HDA API: normalize exceptions and exception codes; Fix ability to history/view a history shared with another user
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/c4f468e43b93/ Changeset: c4f468e43b93 User: carlfeberhard Date: 2014-03-27 17:17:18 Summary: History/HDA API: normalize exceptions and exception codes; Fix ability to history/view a history shared with another user Affected #: 14 files diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/exceptions/__init__.py --- a/lib/galaxy/exceptions/__init__.py +++ b/lib/galaxy/exceptions/__init__.py @@ -68,6 +68,11 @@ status_code = 400 err_code = error_codes.USER_REQUEST_INVALID_PARAMETER +class AuthenticationRequired( MessageException ): + status_code = 403 + #TODO: as 401 and send WWW-Authenticate: ??? + err_code = error_codes.USER_NO_API_KEY + class ItemAccessibilityException( MessageException ): status_code = 403 err_code = error_codes.USER_CANNOT_ACCESS_ITEM @@ -76,6 +81,10 @@ status_code = 403 err_code = error_codes.USER_DOES_NOT_OWN_ITEM +class ConfigDoesNotAllowException( MessageException ): + status_code = 403 + err_code = error_codes.CONFIG_DOES_NOT_ALLOW + class ObjectNotFound( MessageException ): """ Accessed object was not found """ status_code = 404 diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/exceptions/error_codes.json --- a/lib/galaxy/exceptions/error_codes.json +++ b/lib/galaxy/exceptions/error_codes.json @@ -52,7 +52,7 @@ { "name": "USER_NO_API_KEY", "code": 403001, - "message": "API Authentication Required for this request" + "message": "API authentication required for this request" }, { "name": "USER_CANNOT_ACCESS_ITEM", @@ -65,6 +65,11 @@ "message": "User does not own specified item." }, { + "name": "CONFIG_DOES_NOT_ALLOW", + "code": 403004, + "message": "The configuration of this Galaxy instance does not allow that operation" + }, + { "name": "USER_OBJECT_NOT_FOUND", "code": 404001, "message": "No such object found." diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -13,6 +13,7 @@ from paste.httpexceptions import HTTPBadRequest, HTTPInternalServerError from paste.httpexceptions import HTTPNotImplemented, HTTPRequestRangeNotSatisfiable +from galaxy import exceptions from galaxy.exceptions import ItemAccessibilityException, ItemDeletionException, ItemOwnershipException from galaxy.exceptions import MessageException @@ -183,20 +184,22 @@ # should probably be in sep. serializer class/object _used_ by controller def validate_and_sanitize_basestring( self, key, val ): if not isinstance( val, basestring ): - raise ValueError( '%s must be a string or unicode: %s' %( key, str( type( val ) ) ) ) + raise exceptions.RequestParameterInvalidException( '%s must be a string or unicode: %s' + %( key, str( type( val ) ) ) ) return unicode( sanitize_html( val, 'utf-8', 'text/html' ), 'utf-8' ) def validate_and_sanitize_basestring_list( self, key, val ): - if not isinstance( val, list ): - raise ValueError( '%s must be a list of strings: %s' %( key, str( type( val ) ) ) ) try: + assert isinstance( val, list ) return [ unicode( sanitize_html( t, 'utf-8', 'text/html' ), 'utf-8' ) for t in val ] - except TypeError, type_err: - raise ValueError( '%s must be a list of strings: %s' %( key, str( type_err ) ) ) + except ( AssertionError, TypeError ), err: + raise exceptions.RequestParameterInvalidException( '%s must be a list of strings: %s' + %( key, str( type( val ) ) ) ) def validate_boolean( self, key, val ): if not isinstance( val, bool ): - raise ValueError( '%s must be a boolean: %s' %( key, str( type( val ) ) ) ) + raise exceptions.RequestParameterInvalidException( '%s must be a boolean: %s' + %( key, str( type( val ) ) ) ) return val #TODO: @@ -361,7 +364,9 @@ """ Mixin for controllers that use History objects. """ def get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ): - """Get a History from the database by id, verifying ownership.""" + """ + Get a History from the database by id, verifying ownership. + """ history = self.get_object( trans, id, 'History', check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted ) history = self.security_check( trans, history, check_ownership, check_accessible ) diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/web/framework/__init__.py --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -296,7 +296,8 @@ if user_required and trans.anonymous: error_code = error_codes.USER_NO_API_KEY # Use error codes default error message. - return __api_error_response( trans, err_code=error_code, status_code=403 ) + err_msg = "API authentication required for this request" + return __api_error_response( trans, err_code=error_code, err_msg=err_msg, status_code=403 ) if trans.request.body: try: kwargs['payload'] = __extract_payload_from_request(trans, func, kwargs) diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/webapps/galaxy/api/histories.py --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -7,11 +7,6 @@ import pkg_resources pkg_resources.require( "Paste" ) -from paste.httpexceptions import HTTPBadRequest -from paste.httpexceptions import HTTPForbidden -from paste.httpexceptions import HTTPInternalServerError -from paste.httpexceptions import HTTPException - from galaxy import exceptions from galaxy import web from galaxy.web import _future_expose_api as expose_api @@ -24,6 +19,9 @@ from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin +from galaxy.model import orm + +from galaxy import util from galaxy.util import string_as_bool from galaxy.util import restore_text from galaxy.web import url_for @@ -35,6 +33,19 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, ExportsHistoryMixin, ImportsHistoryMixin ): + def __init__( self, app ): + super( HistoriesController, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + histories = HistoryManager() + ) + + def _decode_id( self, trans, id ): + try: + return trans.security.decode_id( id ) + except: + raise exceptions.MalformedId( "Malformed History id ( %s ) specified, unable to decode" + % ( str( id ) ), type='error' ) + @expose_api_anonymous def index( self, trans, deleted='False', **kwd ): """ @@ -55,24 +66,15 @@ rval = [] deleted = string_as_bool( deleted ) - if trans.user: - histories = self.get_user_histories( trans, user=trans.user, only_deleted=deleted ) - - for history in histories: - item = history.to_dict(value_mapper={'id': trans.security.encode_id}) - item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) - rval.append( item ) - - elif trans.galaxy_session.current_history: - #No user, this must be session authentication with an anonymous user. - history = trans.galaxy_session.current_history - item = history.to_dict(value_mapper={'id': trans.security.encode_id}) + histories = self.mgrs.histories.by_user( trans, user=trans.user, only_deleted=deleted ) + for history in histories: + item = history.to_dict( value_mapper={ 'id': trans.security.encode_id } ) item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) - rval.append(item) + rval.append( item ) return rval - @web.expose_api_anonymous + @expose_api_anonymous def show( self, trans, id, deleted='False', **kwd ): """ show( trans, id, deleted='False' ) @@ -82,10 +84,13 @@ return the deleted history with ``id`` * GET /api/histories/most_recently_used: return the most recently used history + * GET /api/histories/current: + return the current (working) history .. note:: Anonymous users are allowed to get their current history :type id: an encoded id string - :param id: the encoded id of the history to query or the string 'most_recently_used' + :param id: the encoded id of the history to query + or the string 'most_recently_used' or the string 'current' :type deleted: boolean :param deleted: if True, allow information on a deleted history to be shown. @@ -93,11 +98,9 @@ :returns: detailed history information from :func:`galaxy.web.base.controller.UsesHistoryDatasetAssociationMixin.get_history_dict` """ - #TODO: GET /api/histories/s/{username}/{slug} history_id = id deleted = string_as_bool( deleted ) - #try: if history_id == "most_recently_used": if not trans.user or len( trans.user.galaxy_sessions ) <= 0: return None @@ -108,25 +111,16 @@ history = trans.get_history( create=True ) else: - history = self.get_history( trans, history_id, check_ownership=False, - check_accessible=True, deleted=deleted ) + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=False, check_accessible=True, deleted=deleted ) + #history = self._get_history( trans, self._decode_id( trans, history_id ), + # check_ownership=False, check_accessible=True, deleted=deleted ) history_data = self.get_history_dict( trans, history ) history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) - - #except HTTPBadRequest, bad_req: - # trans.response.status = 400 - # raise exceptions.MessageException( bad_req.detail ) - # - #except Exception, e: - # msg = "Error in history API at showing history detail: %s" % ( str( e ) ) - # log.exception( msg ) - # trans.response.status = 500 - # return msg - return history_data - @web.expose_api + @expose_api def set_as_current( self, trans, id, **kwd ): """ set_as_current( trans, id, **kwd ) @@ -143,22 +137,11 @@ # added as a non-ATOM API call to support the notion of a 'current/working' history # - unique to the history resource history_id = id - try: - history = self.get_history( trans, history_id, check_ownership=True, check_accessible=True ) - trans.history = history - history_data = self.get_history_dict( trans, history ) - history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) - - except HTTPBadRequest, bad_req: - trans.response.status = 400 - return str( bad_req ) - - except Exception, e: - msg = "Error in history API when switching current history: %s" % ( str( e ) ) - log.exception( msg ) - trans.response.status = 500 - return msg - + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=True, check_accessible=True ) + trans.history = history + history_data = self.get_history_dict( trans, history ) + history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) return history_data @expose_api @@ -196,14 +179,8 @@ new_history = None # if a history id was passed, copy that history if copy_this_history_id: - try: - original_history = self.get_history( trans, copy_this_history_id, - check_ownership=False, check_accessible=True ) - except HTTPBadRequest, bad_request: - trans.response.status = 403 - #TODO: it's either that or parse each possible detail to it's own status code - return { 'error': bad_request.detail or 'Bad request' } - + original_history = self.mgrs.histories.get( trans, self._decode_id( trans, copy_this_history_id ), + check_ownership=False, check_accessible=True ) hist_name = hist_name or ( "Copy of '%s'" % original_history.name ) new_history = original_history.copy( name=hist_name, target_user=trans.user ) @@ -211,20 +188,17 @@ else: new_history = trans.app.model.History( user=trans.user, name=hist_name ) - item = {} - trans.sa_session.add( new_history ) trans.sa_session.flush() - - item = self.get_history_dict( trans, new_history ) - item['url'] = url_for( 'history', id=item['id'] ) - if set_as_current: trans.history = new_history + item = {} + item = self.get_history_dict( trans, new_history ) + item['url'] = url_for( 'history', id=item['id'] ) return item - @web.expose_api + @expose_api def delete( self, trans, id, **kwd ): """ delete( self, trans, id, **kwd ) @@ -253,54 +227,40 @@ purge = string_as_bool( kwd['payload'].get( 'purge', False ) ) rval = { 'id' : history_id } - try: - history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False ) - history.deleted = True + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=True, check_accessible=False ) + history.deleted = True + if purge: + if not trans.app.config.allow_user_dataset_purge and not trans.user_is_admin(): + raise exceptions.ConfigDoesNotAllowException( 'This instance does not allow user dataset purging' ) - if purge: - if not trans.app.config.allow_user_dataset_purge: - raise HTTPForbidden( detail='This instance does not allow user dataset purging' ) + # First purge all the datasets + for hda in history.datasets: + if hda.purged: + continue + hda.purged = True + trans.sa_session.add( hda ) + trans.sa_session.flush() - # First purge all the datasets - for hda in history.datasets: - if hda.purged: - continue - hda.purged = True - trans.sa_session.add( hda ) + if hda.dataset.user_can_purge: + try: + hda.dataset.full_delete() + trans.sa_session.add( hda.dataset ) + except: + pass + # flush now to preserve deleted state in case of later interruption trans.sa_session.flush() - if hda.dataset.user_can_purge: - try: - hda.dataset.full_delete() - trans.sa_session.add( hda.dataset ) - except: - pass - # flush now to preserve deleted state in case of later interruption - trans.sa_session.flush() + # Now mark the history as purged + history.purged = True + self.sa_session.add( history ) + rval[ 'purged' ] = True - # Now mark the history as purged - history.purged = True - self.sa_session.add( history ) - rval[ 'purged' ] = True - - trans.sa_session.flush() - rval[ 'deleted' ] = True - - except HTTPInternalServerError, http_server_err: - log.exception( 'Histories API, delete: uncaught HTTPInternalServerError: %s, %s\n%s', - history_id, str( kwd ), str( http_server_err ) ) - raise - except HTTPException: - raise - except Exception, exc: - log.exception( 'Histories API, delete: uncaught exception: %s, %s\n%s', - history_id, str( kwd ), str( exc ) ) - trans.response.status = 500 - rval.update({ 'error': str( exc ) }) - + trans.sa_session.flush() + rval[ 'deleted' ] = True return rval - @web.expose_api + @expose_api def undelete( self, trans, id, **kwd ): """ undelete( self, trans, id, **kwd ) @@ -314,13 +274,14 @@ :returns: 'OK' if the history was undeleted """ history_id = id - history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False, deleted=True ) + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=True, check_accessible=False, deleted=True ) history.deleted = False trans.sa_session.add( history ) trans.sa_session.flush() return 'OK' - @web.expose_api + @expose_api def update( self, trans, id, payload, **kwd ): """ update( self, trans, id, payload, **kwd ) @@ -340,23 +301,14 @@ any values that were different from the original and, therefore, updated """ #TODO: PUT /api/histories/{encoded_history_id} payload = { rating: rating } (w/ no security checks) - try: - history = self.get_history( trans, id, check_ownership=True, check_accessible=True ) - # validation handled here and some parsing, processing, and conversion - payload = self._validate_and_parse_update_payload( payload ) - # additional checks here (security, etc.) - changed = self.set_history_from_dict( trans, history, payload ) + history_id = id - except Exception, exception: - log.error( 'Update of history (%s) failed: %s', id, str( exception ), exc_info=True ) - # convert to appropo HTTP code - if( isinstance( exception, ValueError ) - or isinstance( exception, AttributeError ) ): - # bad syntax from the validater/parser - trans.response.status = 400 - else: - trans.response.status = 500 - return { 'error': str( exception ) } + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=True, check_accessible=True ) + # validation handled here and some parsing, processing, and conversion + payload = self._validate_and_parse_update_payload( payload ) + # additional checks here (security, etc.) + changed = self.set_history_from_dict( trans, history, payload ) return changed @@ -377,7 +329,8 @@ # PUT instead of POST because multiple requests should just result # in one object being created. history_id = id - history = self.get_history( trans, history_id, check_ownership=False, check_accessible=True ) + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=False, check_accessible=True ) jeha = history.latest_export up_to_date = jeha and jeha.up_to_date if not up_to_date: @@ -389,7 +342,7 @@ if up_to_date and jeha.ready: jeha_id = trans.security.encode_id( jeha.id ) - return dict( download_url=url_for( "history_archive_download", id=history_id, jeha_id=jeha_id ) ) + return dict( download_url=url_for( "history_archive_download", id=id, jeha_id=jeha_id ) ) else: # Valid request, just resource is not ready yet. trans.response.status = "202 Accepted" @@ -408,7 +361,9 @@ """ # Seems silly to put jeha_id in here, but want GET to be immuatable? # and this is being accomplished this way. - history = self.get_history( trans, id, check_ownership=False, check_accessible=True ) + history_id = id + history = self.mgrs.histories.get( trans, trans.security.decode_id( history_id ), + check_ownership=False, check_accessible=True ) matching_exports = filter( lambda e: trans.security.encode_id( e.id ) == jeha_id, history.exports ) if not matching_exports: raise exceptions.ObjectNotFound() @@ -452,3 +407,96 @@ pass #log.warn( 'unknown key: %s', str( key ) ) return validated_payload + + + + +class HistoryManager( object ): + #TODO: all the following would be more useful if passed the user instead of defaulting to trans.user + + def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True, deleted=None ): + """ + Get a History from the database by id, verifying ownership. + """ + # this is a replacement for UsesHistoryMixin because mixins are a bad soln/structure + history = trans.sa_session.query( trans.app.model.History ).get( unencoded_id ) + if history is None: + raise exceptions.ObjectNotFound() + if deleted == True and not history.deleted: + raise exceptions.ItemDeletionException( 'History "%s" is not deleted' % ( history.name ), type="error" ) + elif deleted == False and history.deleted: + raise exceptions.ItemDeletionException( 'History "%s" is deleted' % ( history.name ), type="error" ) + + history = self.secure( trans, history, check_ownership, check_accessible ) + return history + + def by_user( self, trans, user=None, include_deleted=False, only_deleted=False ): + """ + Get all the histories for a given user (defaulting to `trans.user`) + ordered by update time and filtered on whether they've been deleted. + """ + # handle default and/or anonymous user (which still may not have a history yet) + user = user or trans.user + if not user: + current_history = trans.get_history() + return [ current_history ] if current_history else [] + + history_model = trans.model.History + query = ( trans.sa_session.query( history_model ) + .filter( history_model.user == user ) + .order_by( orm.desc( history_model.table.c.update_time ) ) ) + if only_deleted: + query = query.filter( history_model.deleted == True ) + elif not include_deleted: + query = query.filter( history_model.deleted == False ) + return query.all() + + def secure( self, trans, history, check_ownership=True, check_accessible=True ): + """ + checks if (a) user owns item or (b) item is accessible to user. + """ + # all items are accessible to an admin + if trans.user and trans.user_is_admin(): + return history + if check_ownership: + history = self.check_ownership( trans, history ) + if check_accessible: + history = self.check_accessible( trans, history ) + return history + + def is_current( self, trans, history ): + return trans.history == history + + def is_owner( self, trans, history ): + # anon users are only allowed to view their current history + if not trans.user: + return self.is_current( trans, history ) + return trans.user == history.user + + def check_ownership( self, trans, history ): + if trans.user and trans.user_is_admin(): + return history + if not trans.user and not self.is_current( trans, history ): + raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy histories", type='error' ) + if self.is_owner( trans, history ): + return history + raise exceptions.ItemOwnershipException( "History is not owned by the current user", type='error' ) + + def is_accessible( self, trans, history ): + # admin always have access + if trans.user and trans.user_is_admin(): + return True + # owner has implicit access + if self.is_owner( trans, history ): + return True + # importable and shared histories are always accessible + if history.importable: + return True + if trans.user in history.users_shared_with_dot_users: + return True + return False + + def check_accessible( self, trans, history ): + if self.is_accessible( trans, history ): + return history + raise exceptions.ItemAccessibilityException( "History is not accessible to the current user", type='error' ) diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/webapps/galaxy/api/history_contents.py --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -20,6 +20,8 @@ from galaxy.web.base.controller import url_for from galaxy.util.sanitize_html import sanitize_html +from galaxy.webapps.galaxy.api import histories + import logging log = logging.getLogger( __name__ ) @@ -27,6 +29,20 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin, UsesLibraryMixin, UsesLibraryMixinItems, UsesTagsMixin ): + def __init__( self, app ): + super( HistoryContentsController, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + histories = histories.HistoryManager(), + hdas = HDAManager() + ) + + def _decode_id( self, trans, id ): + try: + return trans.security.decode_id( id ) + except: + raise exceptions.MalformedId( "Malformed History id ( %s ) specified, unable to decode" + % ( str( id ) ), type='error' ) + @expose_api_anonymous def index( self, trans, history_id, ids=None, **kwd ): """ @@ -56,53 +72,49 @@ :func:`galaxy.web.base.controller.UsesHistoryDatasetAssociationMixin.get_hda_dict` """ rval = [] - try: - # get the history, if anon user and requesting current history - allow it - if( ( trans.user == None ) - and ( history_id == trans.security.encode_id( trans.history.id ) ) ): - #TODO:?? is secure? - history = trans.history - # otherwise, check permissions for the history first - else: - history = self.get_history( trans, history_id, check_ownership=False, check_accessible=True ) - # Allow passing in type or types - for continuity rest of methods - # take in type - but this one can be passed multiple types and - # type=dataset,dataset_collection is a bit silly. - types = kwd.get( 'type', kwd.get( 'types', None ) ) or [] - if types: - types = util.listify(types) - else: - types = [ 'dataset' ] + # get the history, if anon user and requesting current history - allow it + if( ( trans.user == None ) + and ( history_id == trans.security.encode_id( trans.history.id ) ) ): + history = trans.history + # otherwise, check permissions for the history first + else: + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=False, check_accessible=True ) - contents_kwds = {'types': types} - if ids: - ids = map( lambda id: trans.security.decode_id( id ), ids.split( ',' ) ) - contents_kwds[ 'ids' ] = ids - # If explicit ids given, always used detailed result. - details = 'all' - else: - contents_kwds[ 'deleted' ] = kwd.get( 'deleted', None ) - contents_kwds[ 'visible' ] = kwd.get( 'visible', None ) - # details param allows a mixed set of summary and detailed hdas - #TODO: this is getting convoluted due to backwards compat - details = kwd.get( 'details', None ) or [] - if details and details != 'all': - details = util.listify( details ) + # Allow passing in type or types - for continuity rest of methods + # take in type - but this one can be passed multiple types and + # type=dataset,dataset_collection is a bit silly. + types = kwd.get( 'type', kwd.get( 'types', None ) ) or [] + if types: + types = util.listify(types) + else: + types = [ 'dataset' ] - for content in history.contents_iter( **contents_kwds ): - if isinstance(content, trans.app.model.HistoryDatasetAssociation): - encoded_content_id = trans.security.encode_id( content.id ) - detailed = details == 'all' or ( encoded_content_id in details ) - if detailed: - rval.append( self._detailed_hda_dict( trans, content ) ) - else: - rval.append( self._summary_hda_dict( trans, history_id, content ) ) - except Exception, e: - # for errors that are not specific to one hda (history lookup or summary list) - rval = "Error in history API at listing contents: " + str( e ) - log.error( rval + ": %s, %s" % ( type( e ), str( e ) ), exc_info=True ) - trans.response.status = 500 + contents_kwds = {'types': types} + if ids: + ids = map( lambda id: trans.security.decode_id( id ), ids.split( ',' ) ) + contents_kwds[ 'ids' ] = ids + # If explicit ids given, always used detailed result. + details = 'all' + else: + contents_kwds[ 'deleted' ] = kwd.get( 'deleted', None ) + contents_kwds[ 'visible' ] = kwd.get( 'visible', None ) + # details param allows a mixed set of summary and detailed hdas + #TODO: this is getting convoluted due to backwards compat + details = kwd.get( 'details', None ) or [] + if details and details != 'all': + details = util.listify( details ) + + for content in history.contents_iter( **contents_kwds ): + if isinstance(content, trans.app.model.HistoryDatasetAssociation): + encoded_content_id = trans.security.encode_id( content.id ) + detailed = details == 'all' or ( encoded_content_id in details ) + if detailed: + rval.append( self._detailed_hda_dict( trans, content ) ) + else: + rval.append( self._summary_hda_dict( trans, history_id, content ) ) + return rval #TODO: move to model or Mixin @@ -165,23 +177,20 @@ """ contents_type = kwd.get('type', 'dataset') if contents_type == 'dataset': - return self.__show_dataset( trans, id, history_id, **kwd ) + return self.__show_dataset( trans, id, **kwd ) else: return self.__handle_unknown_contents_type( trans, contents_type ) - def __show_dataset( self, trans, id, history_id, **kwd ): - try: - hda = self.get_history_dataset_association_from_ids( trans, id, history_id ) - hda_dict = self.get_hda_dict( trans, hda ) - hda_dict[ 'display_types' ] = self.get_old_display_applications( trans, hda ) - hda_dict[ 'display_apps' ] = self.get_display_apps( trans, hda ) - return hda_dict - except Exception, e: - msg = "Error in history API at listing dataset: %s" % ( str(e) ) - log.error( msg, exc_info=True ) - trans.response.status = 500 - return msg + def __show_dataset( self, trans, id, **kwd ): + hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), check_ownership=False, check_accessible=True ) + #if hda.history.id != self._decode_id( trans, history_id ): + # raise exceptions.ObjectNotFound( 'dataset was not found in this history' ) + hda_dict = self.get_hda_dict( trans, hda ) + hda_dict[ 'display_types' ] = self.get_old_display_applications( trans, hda ) + hda_dict[ 'display_apps' ] = self.get_display_apps( trans, hda ) + return hda_dict + #TODO: allow anon users to copy hdas, ldas @expose_api def create( self, trans, history_id, payload, **kwd ): """ @@ -207,18 +216,10 @@ :rtype: dict :returns: dictionary containing detailed information for the new HDA """ - #TODO: copy existing, accessible hda - dataset controller, copy_datasets #TODO: convert existing, accessible hda - model.DatasetInstance(or hda.datatype).get_converter_types - # check parameters - # retrieve history - try: - history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False ) - except exceptions.httpexceptions.HTTPException: - raise - except Exception, e: - # no way to tell if it failed bc of perms or other (all MessageExceptions) - trans.response.status = 500 - return str( e ) + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), + check_ownership=True, check_accessible=False ) + type = payload.get('type', 'dataset') if type == 'dataset': return self.__create_dataset( trans, history, payload, **kwd ) @@ -226,53 +227,34 @@ return self.__handle_unknown_contents_type( trans, type ) def __create_dataset( self, trans, history, payload, **kwd ): - source = payload.get('source', None) - content = payload.get('content', None) - if source not in ['library', 'hda'] or content is None: - trans.response.status = 400 - return "Please define the source ('library' or 'hda') and the content." + source = payload.get( 'source', None ) + if source not in ( 'library', 'hda' ): + raise exceptions.RequestParameterInvalidException( + "'source' must be either 'library' or 'hda': %s" %( source ) ) + content = payload.get( 'content', None ) + if content is None: + raise exceptions.RequestParameterMissingException( "'content' id of lda or hda is missing" ) + # copy from library dataset + hda = None if source == 'library': - # get library data set - try: - ld = self.get_library_dataset( trans, content, check_ownership=False, check_accessible=False ) - assert type( ld ) is trans.app.model.LibraryDataset, ( + ld = self.get_library_dataset( trans, content, check_ownership=False, check_accessible=False ) + #TODO: why would get_library_dataset NOT return a library dataset? + if type( ld ) is not trans.app.model.LibraryDataset: + raise exceptions.RequestParameterInvalidException( "Library content id ( %s ) is not a dataset" % content ) - except AssertionError, e: - trans.response.status = 400 - return str( e ) - except Exception, e: - return str( e ) # insert into history hda = ld.library_dataset_dataset_association.to_history_dataset_association( history, add_to_history=True ) - trans.sa_session.flush() - return hda.to_dict() + + # copy an existing, accessible hda elif source == 'hda': - try: - #NOTE: user currently only capable of copying one of their own datasets - hda = self.get_dataset( trans, content, check_ownership=False, check_accessible=True ) - self.security_check( trans, hda.history, check_ownership=False, check_accessible=True ) - except ( exceptions.httpexceptions.HTTPRequestRangeNotSatisfiable, - exceptions.httpexceptions.HTTPBadRequest ), id_exc: - # wot... - trans.response.status = 400 - return str( id_exc ) - except exceptions.MessageException, msg_exc: - #TODO: covers most but not all user exceptions, too generic (403 v.401) - trans.response.status = 403 - return str( msg_exc ) - except Exception, exc: - trans.response.status = 500 - log.exception( "history: %s, source: %s, content: %s", trans.security.encode_id(history.id), source, content ) - return str( exc ) - data_copy=hda.copy( copy_children=True ) - result=history.add_dataset( data_copy ) - trans.sa_session.flush() - return result.to_dict() - else: - # other options - trans.response.status = 501 - return + unencoded_hda_id = self._decode_id( trans, content ) + original = self.mgrs.hdas.get( trans, unencoded_hda_id, check_ownership=False, check_accessible=True ) + data_copy = original.copy( copy_children=True ) + hda = history.add_dataset( data_copy ) + + trans.sa_session.flush() + return hda.to_dict() if hda else None @expose_api_anonymous def update( self, trans, history_id, id, payload, **kwd ): @@ -305,45 +287,38 @@ def __update_dataset( self, trans, history_id, id, payload, **kwd ): changed = {} - try: - # anon user - if trans.user == None: - if history_id != trans.security.encode_id( trans.history.id ): - trans.response.status = 401 - return { 'error': 'Anonymous users cannot edit histories other than their current history' } - anon_allowed_payload = {} - if 'deleted' in payload: - anon_allowed_payload[ 'deleted' ] = payload[ 'deleted' ] - if 'visible' in payload: - anon_allowed_payload[ 'visible' ] = payload[ 'visible' ] - payload = self._validate_and_parse_update_payload( anon_allowed_payload ) - hda = self.get_dataset( trans, id, check_ownership=False, check_accessible=False, check_state=True ) - if hda.history != trans.history: - trans.response.status = 401 - return { 'error': 'Anonymous users cannot edit datasets outside their current history' } + # anon user + if trans.user == None: + if history_id != trans.security.encode_id( trans.history.id ): + raise exceptions.AuthenticationRequired( 'You must be logged in to update this history' ) + anon_allowed_payload = {} + if 'deleted' in payload: + anon_allowed_payload[ 'deleted' ] = payload[ 'deleted' ] + if 'visible' in payload: + anon_allowed_payload[ 'visible' ] = payload[ 'visible' ] + payload = self._validate_and_parse_update_payload( anon_allowed_payload ) - else: - payload = self._validate_and_parse_update_payload( payload ) - # only check_state if not deleting, otherwise cannot delete uploading files - check_state = not payload.get( 'deleted', False ) - hda = self.get_dataset( trans, id, check_ownership=True, check_accessible=True, check_state=check_state ) - # get_dataset can return a string during an error - if hda and isinstance( hda, trans.model.HistoryDatasetAssociation ): - changed = self.set_hda_from_dict( trans, hda, payload ) - if payload.get( 'deleted', False ): - self.stop_hda_creating_job( hda ) + hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), + check_ownership=False, check_accessible=False ) + hda = self.mgrs.hdas.err_if_uploading( trans, hda ) + if hda.history != trans.history: + raise exceptions.AuthenticationRequired( 'You must be logged in to update this dataset' ) - except Exception, exception: - log.error( 'Update of history (%s), HDA (%s) failed: %s', - history_id, id, str( exception ), exc_info=True ) - # convert to appropo HTTP code - if( isinstance( exception, ValueError ) - or isinstance( exception, AttributeError ) ): - # bad syntax from the validater/parser - trans.response.status = 400 - else: - trans.response.status = 500 - return { 'error': str( exception ) } + else: + payload = self._validate_and_parse_update_payload( payload ) + # only check_state if not deleting, otherwise cannot delete uploading files + check_state = not payload.get( 'deleted', False ) + hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), + check_ownership=True, check_accessible=True ) + if check_state: + hda = self.mgrs.hdas.err_if_uploading( trans, hda ) + #hda = self.get_dataset( trans, id, check_ownership=True, check_accessible=True, check_state=check_state ) + + if hda and isinstance( hda, trans.model.HistoryDatasetAssociation ): + changed = self.set_hda_from_dict( trans, hda, payload ) + if payload.get( 'deleted', False ): + self.stop_hda_creating_job( hda ) + return changed #TODO: allow anonymous del/purge and test security on this @@ -387,44 +362,32 @@ # payload takes priority purge = util.string_as_bool( kwd['payload'].get( 'purge', purge ) ) + hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), + check_ownership=True, check_accessible=True ) + self.mgrs.hdas.err_if_uploading( trans, hda ) + rval = { 'id' : id } - try: - hda = self.get_dataset( trans, id, - check_ownership=True, check_accessible=True, check_state=True ) - hda.deleted = True - if purge: - if not trans.app.config.allow_user_dataset_purge: - raise exceptions.httpexceptions.HTTPForbidden( - detail='This instance does not allow user dataset purging' ) - hda.purged = True - trans.sa_session.add( hda ) + hda.deleted = True + if purge: + if not trans.app.config.allow_user_dataset_purge: + raise exceptions.ConfigDoesNotAllowException( 'This instance does not allow user dataset purging' ) + hda.purged = True + trans.sa_session.add( hda ) + trans.sa_session.flush() + if hda.dataset.user_can_purge: + try: + hda.dataset.full_delete() + trans.sa_session.add( hda.dataset ) + except: + pass + # flush now to preserve deleted state in case of later interruption trans.sa_session.flush() - if hda.dataset.user_can_purge: - try: - hda.dataset.full_delete() - trans.sa_session.add( hda.dataset ) - except: - pass - # flush now to preserve deleted state in case of later interruption - trans.sa_session.flush() - rval[ 'purged' ] = True + rval[ 'purged' ] = True - self.stop_hda_creating_job( hda ) + self.stop_hda_creating_job( hda ) + trans.sa_session.flush() - trans.sa_session.flush() - rval[ 'deleted' ] = True - - except exceptions.httpexceptions.HTTPInternalServerError, http_server_err: - log.exception( 'HDA API, delete: uncaught HTTPInternalServerError: %s, %s\n%s', - id, str( kwd ), str( http_server_err ) ) - raise - except exceptions.httpexceptions.HTTPException: - raise - except Exception, exc: - log.exception( 'HDA API, delete: uncaught exception: %s, %s\n%s', - id, str( kwd ), str( exc ) ) - trans.response.status = 500 - rval.update({ 'error': str( exc ) }) + rval[ 'deleted' ] = True return rval def _validate_and_parse_update_payload( self, payload ): @@ -470,3 +433,68 @@ # TODO: raise a message exception instead of setting status and returning dict. trans.response.status = 400 return { 'error': 'Unknown contents type %s' % type } + + +class HDAManager( object ): + + def __init__( self ): + self.histories_mgr = histories.HistoryManager() + + def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True ): + """ + """ + # this is a replacement for UsesHistoryDatasetAssociationMixin because mixins are a bad soln/structure + hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( unencoded_id ) + if hda is None: + raise exceptions.ObjectNotFound() + hda = self.secure( trans, hda, check_ownership, check_accessible ) + return hda + + def secure( self, trans, hda, check_ownership=True, check_accessible=True ): + """ + checks if (a) user owns item or (b) item is accessible to user. + """ + # all items are accessible to an admin + if trans.user and trans.user_is_admin(): + return hda + if check_ownership: + hda = self.check_ownership( trans, hda ) + if check_accessible: + hda = self.check_accessible( trans, hda ) + return hda + + def can_access_dataset( self, trans, hda ): + current_user_roles = trans.get_current_user_roles() + return trans.app.security_agent.can_access_dataset( current_user_roles, hda.dataset ) + + #TODO: is_owner, is_accessible + + def check_ownership( self, trans, hda ): + if not trans.user: + #if hda.history == trans.history: + # return hda + raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy datasets", type='error' ) + if trans.user_is_admin(): + return hda + # check for ownership of the containing history and accessibility of the underlying dataset + if( self.histories_mgr.is_owner( trans, hda.history ) + and self.can_access_dataset( trans, hda ) ): + return hda + raise exceptions.ItemOwnershipException( + "HistoryDatasetAssociation is not owned by the current user", type='error' ) + + def check_accessible( self, trans, hda ): + 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 err_if_uploading( self, trans, hda ): + if hda.state == trans.model.Dataset.states.UPLOAD: + raise exceptions.Conflict( "Please wait until this dataset finishes uploading" ) + return hda diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -860,17 +860,17 @@ hda_dictionaries = [] user_is_owner = False try: - history_to_view = self.get_history( trans, id, False ) + history_to_view = self.get_history( trans, id, check_ownership=False, check_accessible=False ) if not history_to_view: return trans.show_error_message( "The specified history does not exist." ) if history_to_view.user == trans.user: user_is_owner = True + if( ( history_to_view.user != trans.user ) # Admin users can view any history - if( ( history_to_view.user != trans.user ) - and ( not trans.user_is_admin() ) - and ( not history_to_view.importable ) ): - #TODO: no check for shared with + and ( not trans.user_is_admin() ) + and ( not history_to_view.importable ) + and ( trans.user not in history_to_view.users_shared_with_dot_users ) ): return trans.show_error_message( "Either you are not allowed to view this history" + " or the owner of this history has not made it accessible." ) diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/api-anon-history-permission-tests.js --- /dev/null +++ b/test/casperjs/api-anon-history-permission-tests.js @@ -0,0 +1,224 @@ +/* Utility to load a specific page and output html, page text, or a screenshot + * Optionally wait for some time, text, or dom selector + */ +try { + //...if there's a better way - please let me know, universe + var scriptDir = require( 'system' ).args[3] + // remove the script filename + .replace( /[\w|\.|\-|_]*$/, '' ) + // if given rel. path, prepend the curr dir + .replace( /^(?!\/)/, './' ), + spaceghost = require( scriptDir + 'spaceghost' ).create({ + // script options here (can be overridden by CLI) + //verbose: true, + //logLevel: debug, + scriptDir: scriptDir + }); + +} catch( error ){ + console.debug( error ); + phantom.exit( 1 ); +} +spaceghost.start(); + + +// =================================================================== SET UP +var utils = require( 'utils' ); + +var email = spaceghost.user.getRandomEmail(), + password = '123456'; +if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; +} +var inaccessibleHistory, accessibleHistory, publishedHistory, + inaccessibleHdas, accessibleHdas, publishedHdas, + accessibleLink; + +//// ------------------------------------------------------------------------------------------- create 3 histories +spaceghost.user.loginOrRegisterUser( email, password ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + // create three histories: make the 2nd importable (via the API), and the third published + + // make the current the inaccessible one + inaccessibleHistory = this.api.histories.show( 'current' ); + this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); + inaccessibleHistory = this.api.histories.show( 'current' ); + + accessibleHistory = this.api.histories.create({ name: 'accessible' }); + var returned = this.api.histories.update( accessibleHistory.id, { + importable : true + }); + //this.debug( this.jsonStr( returned ) ); + accessibleHistory = this.api.histories.show( accessibleHistory.id ); + + publishedHistory = this.api.histories.create({ name: 'published' }); + returned = this.api.histories.update( publishedHistory.id, { + published : true + }); + //this.debug( this.jsonStr( returned ) ); + publishedHistory = this.api.histories.show( publishedHistory.id ); + +}); + +//// ------------------------------------------------------------------------------------------- upload some files +spaceghost.then( function(){ + this.api.tools.thenUpload( inaccessibleHistory.id, { filepath: this.options.scriptDir + '/../../test-data/1.bed' }); + this.api.tools.thenUpload( accessibleHistory.id, { filepath: this.options.scriptDir + '/../../test-data/1.bed' }); + this.api.tools.thenUpload( publishedHistory.id, { filepath: this.options.scriptDir + '/../../test-data/1.bed' }); +}); +spaceghost.then( function(){ + // check that they're there + inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), + accessibleHdas = this.api.hdas.index( accessibleHistory.id ), + publishedHdas = this.api.hdas.index( publishedHistory.id ); +}); +spaceghost.user.logout(); + +// =================================================================== TESTS +//// ------------------------------------------------------------------------------------------- anon user +function testAnonReadFunctionsOnAccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for ACCESSIBLE history: ' + history.name ); + + // read functions for history + this.test.comment( 'show should work for history: ' + history.name ); + this.test.assert( this.api.histories.show( history.id ).id === history.id, + 'show worked' ); + this.test.comment( 'copying should fail for history (multiple histories not allowed): ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.create({ history_id : history.id }); + }, 403, 'API authentication required for this request', 'update authentication required' ); + + // read functions for history contents + this.test.comment( 'index of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.index( history.id ).length === 1, + 'hda index worked' ); + this.test.comment( 'showing of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.show( history.id, hdas[0].id ).id === hdas[0].id, + 'hda show worked' ); + + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' fails from accessible history (currently login is required): ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.create( this.api.histories.show( 'current' ).id, { + source : 'hda', + content : hdas[0].id + }); + }, 403, 'API authentication required for this request', 'update authentication required' ); +} + +function testAnonReadFunctionsOnInaccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for INACCESSIBLE history: ' + history.name ); + + // read functions for history + this.test.comment( 'show should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.show( history.id ); + }, 403, 'History is not accessible to the current user', 'show failed with error' ); + this.test.comment( 'copying should fail for history (implicit multiple histories): ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.create({ history_id : history.id }); + }, 403, 'API authentication required for this request', 'copy failed with error' ); + + // read functions for history contents + this.test.comment( 'index and show of history contents should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.index( history.id ); + }, 403, 'History is not accessible to the current user', 'hda index failed with error' ); + this.api.assertRaises( function(){ + this.api.hdas.show( history.id, hdas[0].id ); + }, 403, 'History is not accessible to the current user', 'hda show failed with error' ); + + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' from an inaccessible history should fail for: ' + history.name ); + this.api.assertRaises( function(){ + var returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { + source : 'hda', + content : hdas[0].id + }); + this.debug( this.jsonStr( returned ) ); + }, 403, 'API authentication required for this request', 'hda copy from failed with error' ); + +} + +function testAnonWriteFunctions( history, hdas ){ + this.test.comment( '---- testing write/ownership functions for history: ' + history.name ); + + this.test.comment( 'update should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.update( history.id, { deleted: true }); + }, 403, 'API authentication required for this request', 'update authentication required' ); + this.test.comment( 'delete should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.delete_( history.id ); + }, 403, 'API authentication required for this request', 'delete authentication required' ); + this.test.comment( 'set_as_current should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.set_as_current( history.id ); + }, 403, 'API authentication required for this request', 'set_as_current failed with error' ); + + this.test.comment( 'hda updating should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); + // anon hda update fails w/ this msg if trying to update non-current history hda + }, 403, 'You must be logged in to update this history', 'hda update failed with error' ); + this.test.comment( 'hda deletion should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( history.id, hdas[0].id ); + }, 403, 'API authentication required for this request', 'hda delete failed with error' ); + + this.test.comment( 'copying hda into history should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.create( history.id, { + source : 'hda', + // should error before it checks the id + content : 'bler' + }); + }, 403, 'API authentication required for this request', 'hda copy to failed' ); +} + +function testAnonInaccessible( history, hdas ){ + testAnonReadFunctionsOnInaccessible.call( this, history, hdas ); + testAnonWriteFunctions.call( this, history, hdas ); +} + +function testAnonAccessible( history, hdas ){ + testAnonReadFunctionsOnAccessible.call( this, history, hdas ); + testAnonWriteFunctions.call( this, history, hdas ); +} + +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + testAnonInaccessible.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); + testAnonAccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testAnonAccessible.call( spaceghost, publishedHistory, publishedHdas ); +}); + + +// ------------------------------------------------------------------------------------------- user1 revoke perms +spaceghost.user.loginOrRegisterUser( email, password ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + this.test.comment( 'revoking perms should prevent access' ); + this.api.histories.update( accessibleHistory.id, { + importable : false + }); + var returned = this.api.histories.show( accessibleHistory.id ); + + this.api.histories.update( publishedHistory.id, { + importable : false, + published : false + }); + returned = this.api.histories.show( publishedHistory.id ); +}); +spaceghost.user.logout(); + + +// ------------------------------------------------------------------------------------------- anon retry perms +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + testAnonInaccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testAnonInaccessible.call( spaceghost, publishedHistory, publishedHdas ); +}); + + +// =================================================================== +spaceghost.run( function(){ +}); diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/api-anon-history-tests.js --- /dev/null +++ b/test/casperjs/api-anon-history-tests.js @@ -0,0 +1,159 @@ +/* Utility to load a specific page and output html, page text, or a screenshot + * Optionally wait for some time, text, or dom selector + */ +try { + //...if there's a better way - please let me know, universe + var scriptDir = require( 'system' ).args[3] + // remove the script filename + .replace( /[\w|\.|\-|_]*$/, '' ) + // if given rel. path, prepend the curr dir + .replace( /^(?!\/)/, './' ), + spaceghost = require( scriptDir + 'spaceghost' ).create({ + // script options here (can be overridden by CLI) + //verbose: true, + //logLevel: debug, + scriptDir: scriptDir + }); + +} catch( error ){ + console.debug( error ); + phantom.exit( 1 ); +} +spaceghost.start(); + + +// =================================================================== SET UP +var utils = require( 'utils' ); + +// =================================================================== TESTS +spaceghost.thenOpen( spaceghost.baseUrl ).waitForSelector( '.history-name' ); +spaceghost.then( function(){ + + // ------------------------------------------------------------------------------------------- anon allowed + this.test.comment( 'index should get a list of histories' ); + var index = this.api.histories.index(); + this.test.assert( utils.isArray( index ), "index returned an array: length " + index.length ); + this.test.assert( index.length === 1, 'Has at least one history' ); + + this.test.comment( 'show should get a history details object' ); + var historyShow = this.api.histories.show( index[0].id ); + //this.debug( this.jsonStr( historyShow ) ); + this.test.assert( historyShow.id === index[0].id, 'Is the first history' ); + this.test.assert( this.hasKeys( historyShow, [ 'id', 'name', 'user_id' ] ) ); + + this.test.comment( 'Calling show with "current" should return the current history' ); + var current = this.api.histories.show( 'current' ); + //this.debug( this.jsonStr( current ) ); + this.test.assert( current.id === index[0].id, 'Is the first history' ); + this.test.assert( current.id === historyShow.id, 'current returned same id' ); + + + // ------------------------------------------------------------------------------------------- anon forbidden + //TODO: why not return the current history? + this.test.comment( 'calling show with "most_recently_used" should return None for an anon user' ); + var recent = this.api.histories.show( 'most_recently_used' ); + this.test.assert( recent === null, 'most_recently_used returned None' ); + + this.test.comment( 'Calling set_as_current should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.set_as_current( current.id ); + }, 403, 'API authentication required for this request', 'set_as_current failed with error' ); + + this.test.comment( 'Calling create should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.create({ current: true }); + }, 403, 'API authentication required for this request', 'create failed with error' ); + + this.test.comment( 'Calling delete should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.delete_( current.id ); + }, 403, 'API authentication required for this request', 'create failed with error' ); + + this.test.comment( 'Calling update should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.update( current.id, {} ); + }, 403, 'API authentication required for this request', 'update failed with error' ); + + //TODO: need these two in api.js + //this.test.comment( 'Calling archive_import should fail for an anonymous user' ); + //this.api.assertRaises( function(){ + // this.api.histories.archive_import( current.id, {} ); + //}, 403, 'API authentication required for this request', 'archive_import failed with error' ); + + //this.test.comment( 'Calling archive_download should fail for an anonymous user' ); + //this.api.assertRaises( function(){ + // this.api.histories.archive_download( current.id, {} ); + //}, 403, 'API authentication required for this request', 'archive_download failed with error' ); + + // test server bad id protection + spaceghost.test.comment( 'A bad id should throw an error' ); + this.api.assertRaises( function(){ + this.api.histories.show( '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); + +}); + +// ------------------------------------------------------------------------------------------- hdas +spaceghost.thenOpen( spaceghost.baseUrl ).waitForSelector( '.history-name' ); +//TODO: can't use this - get a 400 when tools checks for history: 'logged in to manage' +//spaceghost.then( function(){ +// this.api.tools.thenUpload( spaceghost.api.histories.show( 'current' ).id, { +// filepath: this.options.scriptDir + '/../../test-data/1.sam' +// }); +//}); +spaceghost.then( function(){ + spaceghost.tools.uploadFile( '../../test-data/1.sam', function( uploadInfo ){ + this.test.assert( uploadInfo.hdaElement !== null, "Convenience function produced hda" ); + var state = this.historypanel.getHdaState( '#' + uploadInfo.hdaElement.attributes.id ); + this.test.assert( state === 'ok', "Convenience function produced hda in ok state" ); + }); +}); + +spaceghost.then( function(){ + var current = this.api.histories.show( 'current' ); + + // ------------------------------------------------------------------------------------------- anon allowed + this.test.comment( 'anonymous users can index hdas in their current history' ); + var hdaIndex = this.api.hdas.index( current.id ); + this.test.assert( hdaIndex.length === 1, 'indexed hdas' ); + + this.test.comment( 'anonymous users can show hdas in their current history' ); + var hda = this.api.hdas.show( current.id, hdaIndex[0].id ); + this.test.assert( this.hasKeys( hda, [ 'id', 'name' ] ), 'showed hda: ' + hda.name ); + + this.test.comment( 'anonymous users can hide hdas in their current history' ); + var changed = this.api.hdas.update( current.id, hda.id, { visible: false }); + hda = this.api.hdas.show( current.id, hda.id ); + this.test.assert( hda.visible === false, 'successfully hidden' ); + + this.test.comment( 'anonymous users can mark their hdas as deleted in their current history' ); + changed = this.api.hdas.update( current.id, hda.id, { deleted: true }); + hda = this.api.hdas.show( current.id, hda.id ); + this.test.assert( hda.deleted, 'successfully deleted' ); + + // ------------------------------------------------------------------------------------------- anon forbidden + //TODO: should be allowed... + this.test.comment( 'Calling create should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.hdas.create( current.id, { source: 'hda', content: 'doesntmatter' }); + }, 403, 'API authentication required for this request', 'create failed with error' ); + + //TODO: should be allowed (along with purge) and automatically creates new history (as UI) + this.test.comment( 'Calling delete should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( current.id, hda.id ); + }, 403, 'API authentication required for this request', 'delete failed with error' ); + + //TODO: only sharing, tags, annotations should be blocked/prevented + this.test.comment( 'Calling update with keys other than "visible" or "deleted" should fail silently' ); + changed = this.api.hdas.update( current.id, hda.id, { tags: [ 'one' ] }); + hda = this.api.hdas.show( current.id, hda.id ); + this.debug( this.jsonStr( hda.tags ) ); + + this.test.assert( hda.tags.length === 0, 'tags were not set' ); + +}); + +// =================================================================== +spaceghost.run( function(){ +}); diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/api-hda-tests.js --- a/test/casperjs/api-hda-tests.js +++ b/test/casperjs/api-hda-tests.js @@ -55,6 +55,7 @@ 'metadata_comment_lines', 'metadata_data_lines' ]; +// ------------------------------------------------------------------------------------------- logged in user spaceghost.then( function(){ // ------------------------------------------------------------------------------------------- INDEX @@ -299,7 +300,7 @@ this.test.comment( 'create should error with "Please define the source" when the param "from_ld_id" is not used' ); this.api.assertRaises( function(){ this.api.hdas.create( lastHistory.id, { bler: 'bler' } ); - }, 400, 'Please define the source', 'create with no source failed' ); + }, 400, "must be either 'library' or 'hda'", 'create with no source failed' ); this.test.comment( 'updating using a nonsense key should fail silently' ); returned = this.api.hdas.update( lastHistory.id, hdaShow.id, { @@ -310,15 +311,15 @@ spaceghost.test.comment( 'A bad id should throw an error when using show' ); this.api.assertRaises( function(){ this.api.hdas.show( lastHistory.id, '1234123412341234' ); - }, 500, 'unable to decode', 'Bad Request with invalid id: show' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); spaceghost.test.comment( 'A bad id should throw an error when using update' ); this.api.assertRaises( function(){ this.api.hdas.update( lastHistory.id, '1234123412341234', {} ); - }, 400, 'invalid literal for int', 'Bad Request with invalid id: update' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: update' ); spaceghost.test.comment( 'A bad id should throw an error when using delete' ); this.api.assertRaises( function(){ this.api.hdas.delete_( lastHistory.id, '1234123412341234' ); - }, 500, 'invalid literal for int', 'Bad Request with invalid id: delete' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: delete' ); spaceghost.test.comment( 'A bad id should throw an error when using undelete' ); this.test.comment( 'updating by attempting to change type should cause an error' ); @@ -362,6 +363,8 @@ /* */ }); +//spaceghost.user.logout(); + // =================================================================== spaceghost.run( function(){ diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/api-history-permission-tests.js --- a/test/casperjs/api-history-permission-tests.js +++ b/test/casperjs/api-history-permission-tests.js @@ -60,6 +60,7 @@ this.test.assert( inaccessibleHistory.published === false, 'initial published is false: ' + inaccessibleHistory.published ); this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); + inaccessibleHistory = this.api.histories.show( 'current' ); this.test.comment( 'Setting importable to true should create a slug, username_and_slug, and importable === true' ); accessibleHistory = this.api.histories.create({ name: 'accessible' }); @@ -78,7 +79,7 @@ this.test.assert( accessibleHistory.username_and_slug === accessibleLink, 'username_and_slug is proper: ' + accessibleHistory.username_and_slug ); - this.test.comment( 'Setting published to true should create make accessible and published === true' ); + this.test.comment( 'Setting published to true should make accessible and published === true' ); publishedHistory = this.api.histories.create({ name: 'published' }); returned = this.api.histories.update( publishedHistory.id, { published : true @@ -110,6 +111,7 @@ inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), accessibleHdas = this.api.hdas.index( accessibleHistory.id ), publishedHdas = this.api.hdas.index( publishedHistory.id ); + this.test.comment( '---- adding datasets' ); this.test.assert( inaccessibleHdas.length === 1, 'uploaded file to inaccessible: ' + inaccessibleHdas.length ); this.test.assert( accessibleHdas.length === 1, @@ -120,149 +122,119 @@ spaceghost.user.logout(); //// ------------------------------------------------------------------------------------------- log in user2 -function ensureInaccessibility( history, hdas ){ +function testReadFunctionsOnAccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for ACCESSIBLE history: ' + history.name ); - this.test.comment( 'all four CRUD API calls should fail for user2 with history: ' + history.name ); + // read functions for history + this.test.comment( 'show should work for history: ' + history.name ); + this.test.assert( this.api.histories.show( history.id ).id === history.id, + 'show worked' ); + this.test.comment( 'copying should work for history: ' + history.name ); + var returned = this.api.histories.create({ history_id : history.id }); + this.test.assert( returned.name === "Copy of '" + history.name + "'", + 'copied name matches: ' + returned.name ); + + // read functions for history contents + this.test.comment( 'index of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.index( history.id ).length === 1, + 'hda index worked' ); + this.test.comment( 'showing of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.show( history.id, hdas[0].id ).id === hdas[0].id, + 'hda show worked' ); + + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' should work from accessible history: ' + history.name ); + returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { + source : 'hda', + content : hdas[0].id + }); + this.test.assert( returned.name === hdas[0].name, 'successful hda copy from: ' + returned.name ); +} + +function testReadFunctionsOnInaccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for INACCESSIBLE history: ' + history.name ); + + // read functions for history + this.test.comment( 'show should fail for history: ' + history.name ); this.api.assertRaises( function(){ this.api.histories.show( history.id ); - }, 400, 'History is not accessible to the current user', 'show failed with error' ); + }, 403, 'History is not accessible to the current user', 'show failed with error' ); + this.test.comment( 'copying should fail for history: ' + history.name ); this.api.assertRaises( function(){ this.api.histories.create({ history_id : history.id }); }, 403, 'History is not accessible to the current user', 'copy failed with error' ); - this.api.assertRaises( function(){ - this.api.histories.update( history.id, { deleted: true }); - }, 500, 'History is not owned by the current user', 'update failed with error' ); - this.api.assertRaises( function(){ - this.api.histories.delete_( history.id ); - }, 400, 'History is not owned by the current user', 'delete failed with error' ); - this.test.comment( 'all four HDA CRUD API calls should fail for user2 with history: ' + history.name ); + // read functions for history contents + this.test.comment( 'index and show of history contents should fail for history: ' + history.name ); this.api.assertRaises( function(){ this.api.hdas.index( history.id ); - }, 500, 'History is not accessible to the current user', 'index failed with error' ); + }, 403, 'History is not accessible to the current user', 'hda index failed with error' ); this.api.assertRaises( function(){ this.api.hdas.show( history.id, hdas[0].id ); - }, 500, 'History is not accessible to the current user', 'show failed with error' ); - this.api.assertRaises( function(){ - this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); - }, 500, 'HistoryDatasetAssociation is not owned by current user', 'update failed with error' ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( history.id, hdas[0].id ); - }, 500, 'HistoryDatasetAssociation is not owned by current user', 'delete failed with error' ); + }, 403, 'History is not accessible to the current user', 'hda show failed with error' ); this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' - + 'from an inaccessible history should fail' ); + + ' from an inaccessible history should fail for: ' + history.name ); this.api.assertRaises( function(){ var returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { source : 'hda', content : hdas[0].id }); this.debug( this.jsonStr( returned ) ); - }, 403, 'History is not accessible to the current user', 'copy failed with error' ); + }, 403, 'History is not accessible to the current user', 'hda copy from failed with error' ); } -spaceghost.user.loginOrRegisterUser( email2, password2 ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - // ----------------------------------------------------------------------------------------- user2 + inaccessible - ensureInaccessibility.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); - - // ----------------------------------------------------------------------------------------- user2 + accessible - this.test.comment( 'show should work for the importable history' ); - this.test.assert( this.api.histories.show( accessibleHistory.id ).id === accessibleHistory.id, - 'show worked' ); +function testWriteFunctions( history, hdas ){ + this.test.comment( '---- testing write/ownership functions for history: ' + history.name ); - this.test.comment( 'create/copy should work for the importable history' ); - var returned = this.api.histories.create({ history_id : accessibleHistory.id }); - this.test.assert( returned.name === "Copy of '" + accessibleHistory.name + "'", - 'copied name matches: ' + returned.name ); + this.test.comment( 'update should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.update( history.id, { deleted: true }); + }, 403, 'History is not owned by the current user', 'update failed with error' ); + this.test.comment( 'delete should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.delete_( history.id ); + }, 403, 'History is not owned by the current user', 'delete failed with error' ); + this.test.comment( 'set_as_current should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.set_as_current( history.id ); + }, 403, 'History is not owned by the current user', 'set_as_current failed with error' ); - this.test.comment( 'update should fail for the importable history' ); + this.test.comment( 'hda updating should fail for history: ' + history.name ); this.api.assertRaises( function(){ - this.api.histories.update( accessibleHistory.id, { deleted: true }); - }, 500, 'History is not owned by the current user', 'update failed with error' ); - this.test.comment( 'delete should fail for the importable history' ); + this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); + }, 403, 'HistoryDatasetAssociation is not owned by the current user', 'hda update failed with error' ); + this.test.comment( 'hda deletion should fail for history: ' + history.name ); this.api.assertRaises( function(){ - this.api.histories.delete_( accessibleHistory.id ); - }, 400, 'History is not owned by the current user', 'delete failed with error' ); + this.api.hdas.delete_( history.id, hdas[0].id ); + }, 403, 'HistoryDatasetAssociation is not owned by the current user', 'hda delete failed with error' ); - this.test.comment( 'indexing should work for the contents of the importable history' ); - this.test.assert( this.api.hdas.index( accessibleHistory.id ).length === 1, - 'index worked' ); - this.test.comment( 'showing should work for the contents of the importable history' ); - this.test.assert( this.api.hdas.show( accessibleHistory.id, accessibleHdas[0].id ).id === accessibleHdas[0].id, - 'show worked' ); - this.test.comment( 'updating should fail for the contents of the importable history' ); + this.test.comment( 'copying hda into history should fail for history: ' + history.name ); this.api.assertRaises( function(){ - this.api.hdas.update( accessibleHistory.id, accessibleHdas[0].id, { deleted: true }); - }, 500, 'HistoryDatasetAssociation is not owned by current user', 'update failed with error' ); - this.test.comment( 'deleting should fail for the contents of the importable history' ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( accessibleHistory.id, accessibleHdas[0].id ); - }, 500, 'HistoryDatasetAssociation is not owned by current user', 'delete failed with error' ); - this.test.comment( 'copying a dataset from the importable history should work' ); - returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { - source : 'hda', - content : accessibleHdas[0].id - }); - this.test.assert( returned.name === accessibleHdas[0].name, 'successful copy from: ' + returned.name ); - - this.test.comment( 'copying a dataset into the importable history should fail' ); - this.api.assertRaises( function(){ - this.api.hdas.create( accessibleHistory.id, { + this.api.hdas.create( history.id, { source : 'hda', // should error before it checks the id content : 'bler' }); - }, 400, 'History is not owned by the current user', 'copy to failed' ); + }, 403, 'History is not owned by the current user', 'hda copy to failed' ); +} - //// ----------------------------------------------------------------------------------------- user2 + published - this.test.comment( 'show should work for the published history' ); - this.test.assert( this.api.histories.show( publishedHistory.id ).id === publishedHistory.id, - 'show worked' ); - this.test.comment( 'create/copy should work for the published history' ); - returned = this.api.histories.create({ history_id : publishedHistory.id }); - this.test.assert( returned.name === "Copy of '" + publishedHistory.name + "'", - 'copied name matches: ' + returned.name ); - this.test.comment( 'update should fail for the published history' ); - this.api.assertRaises( function(){ - this.api.histories.update( publishedHistory.id, { deleted: true }); - }, 500, 'History is not owned by the current user', 'update failed with error' ); - this.test.comment( 'delete should fail for the published history' ); - this.api.assertRaises( function(){ - this.api.histories.delete_( publishedHistory.id ); - }, 400, 'History is not owned by the current user', 'delete failed with error' ); +function testInaccessible( history, hdas ){ + testReadFunctionsOnInaccessible.call( this, history, hdas ); + testWriteFunctions.call( this, history, hdas ); +} - this.test.comment( 'indexing should work for the contents of the published history' ); - this.test.assert( this.api.hdas.index( publishedHistory.id ).length === 1, - 'index worked' ); - this.test.comment( 'showing should work for the contents of the published history' ); - this.test.assert( this.api.hdas.show( publishedHistory.id, publishedHdas[0].id ).id === publishedHdas[0].id, - 'show worked' ); - this.test.comment( 'updating should fail for the contents of the published history' ); - this.api.assertRaises( function(){ - this.api.hdas.update( publishedHistory.id, publishedHdas[0].id, { deleted: true }); - }, 500, 'HistoryDatasetAssociation is not owned by current user', 'update failed with error' ); - this.test.comment( 'deleting should fail for the contents of the published history' ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( publishedHistory.id, publishedHdas[0].id ); - }, 500, 'HistoryDatasetAssociation is not owned by current user', 'delete failed with error' ); +function testAccessible( history, hdas ){ + testReadFunctionsOnAccessible.call( this, history, hdas ); + testWriteFunctions.call( this, history, hdas ); +} - this.test.comment( 'copying a dataset from the published history should work' ); - returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { - source : 'hda', - content : publishedHdas[0].id - }); - this.test.assert( returned.name === publishedHdas[0].name, 'successful copy from: ' + returned.name ); - - this.test.comment( 'copying a dataset into the published history should fail' ); - this.api.assertRaises( function(){ - this.api.hdas.create( publishedHistory.id, { - source : 'hda', - // should error before it checks the id - content : 'bler' - }); - }, 400, 'History is not owned by the current user', 'copy to failed' ); +spaceghost.user.loginOrRegisterUser( email2, password2 ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + testInaccessible.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); + testAccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testAccessible.call( spaceghost, publishedHistory, publishedHdas ); }); spaceghost.user.logout(); @@ -294,9 +266,11 @@ //// ------------------------------------------------------------------------------------------- user2 retry perms spaceghost.user.loginOrRegisterUser( email2, password2 ); spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - ensureInaccessibility.call( spaceghost, accessibleHistory, accessibleHdas ); - ensureInaccessibility.call( spaceghost, publishedHistory, publishedHdas ); + testInaccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testInaccessible.call( spaceghost, publishedHistory, publishedHdas ); }); +spaceghost.user.logout(); + // =================================================================== spaceghost.run( function(){ diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/api-history-tests.js --- a/test/casperjs/api-history-tests.js +++ b/test/casperjs/api-history-tests.js @@ -316,7 +316,11 @@ spaceghost.test.comment( 'A bad id should throw an error when using update' ); this.api.assertRaises( function(){ this.api.histories.update( '1234123412341234', {} ); - }, 500, 'unable to decode', 'Bad Request with invalid id: update' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: update' ); + spaceghost.test.comment( 'A bad id should throw an error when using set_as_current' ); + this.api.assertRaises( function(){ + this.api.histories.set_as_current( '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: set_as_current' ); spaceghost.test.comment( 'A bad id should throw an error when using delete' ); this.api.assertRaises( function(){ this.api.histories.delete_( '1234123412341234' ); @@ -346,6 +350,11 @@ returned = spaceghost.api.histories.update( newFirstHistory.id, { tags: badVal }); }, 400, 'tags must be a list', 'type validation error' ); }); + + this.test.comment( 'calling show with /deleted should raise a bad request' ); + this.api.assertRaises( function(){ + this.api.histories.show( newFirstHistory.id, true ); + }, 400, 'is not deleted', 'Bad Request returned for non-deleted' ); /* */ //this.debug( this.jsonStr( historyShow ) ); diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/casperjs_runner.py --- a/test/casperjs/casperjs_runner.py +++ b/test/casperjs/casperjs_runner.py @@ -377,19 +377,19 @@ class Test_04_HDAs( CasperJSTestCase ): """Tests for HistoryDatasetAssociation fetching, rendering, and modeling. """ - #def test_00_HDA_states( self ): - # """Test structure rendering of HDAs in all the possible HDA states - # """ - # self.run_js_script( 'hda-state-tests.js' ) + def test_00_HDA_states( self ): + """Test structure rendering of HDAs in all the possible HDA states + """ + self.run_js_script( 'hda-state-tests.js' ) class Test_05_API( CasperJSTestCase ): """Tests for API functionality and security. """ - #def test_00_history_api( self ): - # """Test history API. - # """ - # self.run_js_script( 'api-history-tests.js' ) + def test_00_history_api( self ): + """Test history API. + """ + self.run_js_script( 'api-history-tests.js' ) def test_01_hda_api( self ): """Test HDA API. @@ -401,6 +401,16 @@ """ self.run_js_script( 'api-history-permission-tests.js' ) + def test_03_anon_history_api( self ): + """Test API for histories using anonymous user. + """ + self.run_js_script( 'api-anon-history-tests.js' ) + + def test_04_anon_history_api( self ): + """Test API permissions for importable, published histories using anonymous user. + """ + self.run_js_script( 'api-anon-history-permission-tests.js' ) + # ==================================================================== MAIN if __name__ == '__main__': diff -r b6d04f39a37fd73abd9c0c5e9695d74330786f47 -r c4f468e43b93406804bd7665a2755b16f8bac463 test/casperjs/modules/api.js --- a/test/casperjs/modules/api.js +++ b/test/casperjs/modules/api.js @@ -75,8 +75,8 @@ if( resp.status !== 200 ){ // grrr... this doesn't lose the \n\r\t //throw new APIError( resp.responseText.replace( /[\s\n\r\t]+/gm, ' ' ).replace( /"/, '' ) ); - this.spaceghost.debug( 'API error: code: ' + resp.status + ', responseText: ' + resp.responseText ); - this.spaceghost.debug( '\t responseJSON: ' + this.spaceghost.jsonStr( resp.responseJSON ) ); + this.spaceghost.debug( 'API error: code: ' + resp.status + ', response:\n' + + ( resp.responseJSON? this.spaceghost.jsonStr( resp.responseJSON ) : resp.responseText ) ); throw new APIError( resp.responseText, resp.status ); } return JSON.parse( resp.responseText ); @@ -95,8 +95,6 @@ try { testFn.call( this.spaceghost ); } catch( err ){ - this.spaceghost.debug( err.name + ': ' + err.status ); - this.spaceghost.debug( err.message ); if( ( err.name === 'APIError' ) && ( err.status && err.status === statusExpected ) && ( err.message.indexOf( errMsgContains ) !== -1 ) ){ @@ -525,14 +523,13 @@ } var response = this.api.spaceghost.evaluate( function( url, historyId, inputs ){ var file = $( 'input[name="casperjs-upload-file"]' )[0].files[0], - formData = new FormData(), - response; + formData = new FormData(); formData.append( 'files_0|file_data', file ); formData.append( 'history_id', historyId ); formData.append( 'tool_id', 'upload1' ); formData.append( 'inputs', JSON.stringify( inputs ) ); - $.ajax({ + return $.ajax({ url : url, async : false, type : 'POST', @@ -543,18 +540,17 @@ processData : false, // if we don't add this, payload isn't processed as JSON headers : { 'Accept': 'application/json' } - }).done(function( resp ){ - response = resp; }); - // this works only bc jq is async - return response; }, utils.format( this.urlTpls.create ), historyId, inputs ); - if( !response ){ - throw new APIError( 'Failed to upload: ' + options.filepath, 0 ); + if( response.status !== 200 ){ + // grrr... this doesn't lose the \n\r\t + //throw new APIError( response.responseText.replace( /[\s\n\r\t]+/gm, ' ' ).replace( /"/, '' ) ); + this.api.spaceghost.debug( 'API error: code: ' + response.status + ', response:\n' + + ( response.responseJSON? this.api.spaceghost.jsonStr( response.responseJSON ) : response.responseText ) ); + throw new APIError( response.responseText, response.status ); } - - return response; + return JSON.parse( response.responseText ); }; /** amount of time allowed to upload a file (before erroring) */ Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org