API, batch: add allowed_list to narrow use

- only allows processing batch calls to the urls that match the regexs
in allowed_list controlling/decreasing the scope of the middleware
This commit is contained in:
carlfeberhard
2016-07-14 15:43:24 -04:00
parent 7bfd6410eb
commit 6825f4c5f4
8 changed files with 55508 additions and 47 deletions
+35 -4
View File
@@ -3,7 +3,10 @@
import io
from urlparse import urlparse
import json
import re
from paste import httpexceptions
import routes
import logging
log = logging.getLogger( __name__ )
@@ -13,7 +16,11 @@ class BatchMiddleware( object ):
"""
"""
DEFAULT_CONFIG = {
'route' : '/api/batch'
'route' : '/api/batch',
'allowed_routes' : [
'^api\/users.*',
'^api\/histories.*',
]
}
def __init__( self, galaxy, application, config=None ):
@@ -21,7 +28,9 @@ class BatchMiddleware( object ):
self.galaxy = galaxy
#: the wrapped webapp
self.application = application
self.config = config or self.DEFAULT_CONFIG
self.config = self.DEFAULT_CONFIG.copy()
self.config.update( config )
self.base_url = routes.url_for( '/' )
self.handle_request = self.galaxy.handle_request
def __call__( self, environ, start_response ):
@@ -35,7 +44,10 @@ class BatchMiddleware( object ):
responses = []
for request in requests:
if not self._valid( request ):
print '------------------------'
print request
if not self._is_allowed_route( request[ 'url' ] ):
responses.append( self._disallowed_route_response( request[ 'url' ] ) )
continue
request_environ = self._build_request_environ( batch_environ, request )
@@ -57,9 +69,28 @@ class BatchMiddleware( object ):
payload = json.loads( request_body )
return payload
def _valid( self, request ):
def _is_allowed_route( self, route ):
if self.config.get( 'allowed_routes', None ):
print self.base_url
shortened_route = route.replace( self.base_url, '', 1 )
matches = [ re.match( allowed, shortened_route ) for allowed in self.config[ 'allowed_routes' ] ]
print matches
return any( matches )
return True
def _disallowed_route_response( self, route ):
return dict( status=403, headers=self._default_headers(), body={
'err_msg' : 'Disallowed route used for batch operation',
'route' : route,
'allowed' : self.config[ 'allowed_routes' ]
})
def _create_invalid_batch_response( self ):
return dict( status=403, headers=self._default_headers(), body={
"err_msg" : "Disallowed route used for batch operation",
"allowed" : self.allowed_routes,
})
def _build_request_environ( self, original_environ, request ):
"""
Given a request and the original environ used to call the batch, return
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+56 -1
View File
@@ -1,2 +1,57 @@
webpackJsonp([4],[function(e,r,n){(function(e){var r=n(1),a=r,i=n(58).GalaxyApp,o=n(11),t=n(5),l=n(60);window.app=function(n,c){window.Galaxy=new i(n,c),Galaxy.debug("login app");var d=encodeURI(n.redirect);if(!n.show_welcome_with_login){var w=r.param({use_panels:"True",redirect:d});return void(window.location.href=Galaxy.root+"user/login?"+w)}var p=new l.PageLayoutView(e.extend(n,{el:"body",center:new o.CenterPanel({el:"#center"}),right:new o.RightPanel({title:t("Login required"),el:"#right"})}));a(function(){var e=r.param({redirect:d}),a=Galaxy.root+"user/login?"+e;p.render(),p.center.$("#galaxy_main").prop("src",n.welcome_url),p.right.$(".unified-panel-body").css("overflow","hidden").html('<iframe src="'+a+'" frameborder="0" style="width: 100%; height: 100%;"/>')})}}).call(r,n(2))}]);
webpackJsonp([5],[
/* 0 */
/*!**************************************!*\
!*** ./galaxy/scripts/apps/login.js ***!
\**************************************/
/***/ function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {
var jQuery = __webpack_require__( /*! jquery */ 3 ),
$ = jQuery,
GalaxyApp = __webpack_require__( /*! galaxy */ 4 ).GalaxyApp,
PANEL = __webpack_require__( /*! layout/panel */ 12 ),
_l = __webpack_require__( /*! utils/localization */ 7 ),
PAGE = __webpack_require__( /*! layout/page */ 98 );
window.app = function app( options, bootstrapped ){
window.Galaxy = new GalaxyApp( options, bootstrapped );
Galaxy.debug( 'login app' );
var redirect = encodeURI( options.redirect );
// TODO: remove iframe for user login (at least) and render login page from here
// then remove this redirect
if( !options.show_welcome_with_login ){
var params = jQuery.param({ use_panels : 'True', redirect : redirect });
window.location.href = Galaxy.root + 'user/login?' + params;
return;
}
var loginPage = new PAGE.PageLayoutView( _.extend( options, {
el : 'body',
center : new PANEL.CenterPanel({ el : '#center' }),
right : new PANEL.RightPanel({
title : _l( 'Login required' ),
el : '#right'
}),
}));
$(function(){
// TODO: incorporate *actual* referrer/redirect info as the original page does
var params = jQuery.param({ redirect : redirect }),
loginUrl = Galaxy.root + 'user/login?' + params;
loginPage.render();
// welcome page (probably) needs to remain sandboxed
loginPage.center.$( '#galaxy_main' ).prop( 'src', options.welcome_url );
loginPage.right.$( '.unified-panel-body' )
.css( 'overflow', 'hidden' )
.html( '<iframe src="' + loginUrl + '" frameborder="0" style="width: 100%; height: 100%;"/>' );
});
};
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1)))
/***/ }
]);
//# sourceMappingURL=login.bundled.js.map
File diff suppressed because one or more lines are too long
+39 -12
View File
@@ -1,11 +1,9 @@
import json
from requests import post
import pprint
from base import api
# from .helpers import DatasetPopulator
import logging
from requests import post
from base import api
log = logging.getLogger( "functional_tests.py" )
@@ -15,15 +13,17 @@ class ApiBatchTestCase( api.ApiTestCase ):
return self.galaxy_interactor.api_key if not admin else self.galaxy_interactor.master_api_key
def _with_key( self, url, admin=False ):
return url + '?key=' + self._get_api_key( admin=admin )
sep = '&' if '?' in url else '?'
return url + sep + 'key=' + self._get_api_key( admin=admin )
def _post_batch( self, batch ):
data = json.dumps({ "batch" : batch })
return post( "%s/batch" % ( self.galaxy_interactor.api_url ), data=data )
def log_reponse( self, response ):
def _log_reponse( self, response ):
log.debug( 'RESPONSE %s\n%s', ( '-' * 40 ), pprint.pformat( response ) )
# ---- tests
def test_simple_array( self ):
batch = [
dict( url=self._with_key( '/api/histories' ) ),
@@ -33,17 +33,26 @@ class ApiBatchTestCase( api.ApiTestCase ):
]
response = self._post_batch( batch )
response = response.json()
# self.log_reponse( response )
# self._log_reponse( response )
self.assertIsInstance( response, list )
self.assertEquals( len( response ), 3 )
def test_bad_route( self ):
def test_unallowed_route( self ):
batch = [
dict( url=self._with_key( '/api/bler' ) )
dict( url=self._with_key( '/api/workflow' ) )
]
response = self._post_batch( batch )
response = response.json()
self.assertIsInstance( response, list )
self.assertEquals( response[0][ 'status' ], 403 )
def test_404_route( self ):
# needs to be within the allowed routes
batch = [
dict( url=self._with_key( '/api/histories_bler' ) )
]
response = self._post_batch( batch )
response = response.json()
# self.log_reponse( response )
self.assertIsInstance( response, list )
self.assertEquals( response[0][ 'status' ], 404 )
@@ -54,7 +63,25 @@ class ApiBatchTestCase( api.ApiTestCase ):
]
response = self._post_batch( batch )
response = response.json()
# self.log_reponse( response )
# self._log_reponse( response )
self.assertIsInstance( response, list )
self.assertEquals( response[0][ 'status' ], 400 )
self.assertEquals( response[1][ 'status' ], 501 )
def test_querystring_params( self ):
post_data = dict( name='test' )
create_response = self._post( 'histories', data=post_data ).json()
history_url = '/api/histories/' + create_response[ 'id' ]
history_url_with_keys = history_url + '?v=dev&keys=size,non_ready_jobs'
contents_url_with_filters = history_url + '/contents?v=dev&q=deleted&qv=True'
batch = [
dict( url=self._with_key( history_url_with_keys ) ),
dict( url=self._with_key( contents_url_with_filters ) ),
]
response = self._post_batch( batch )
response = response.json()
self._log_reponse( response )
self.assertEquals( len( response ), 2 )
self.assertEquals( len( response[0][ 'body' ].keys() ), 2 )
self.assertEquals( response[1][ 'body' ], [] )