Add CORS hook to api application
A new config option was added called 'allowed_cors_origins' which takes a list of domains where Cross Origin Resource Sharing is permitted. If a request is in this list of sites, CORS headers are added to the responses by the hook in order to allow resource sharing. This is needed by the UI to be able to retrieve data from the API. Change-Id: I7d98d15b05e8f0da8af3a24a424779aa5be788d5
This commit is contained in:
parent
8f972ce693
commit
caf52730e7
|
@ -116,6 +116,10 @@
|
|||
# relative the project root. (string value)
|
||||
#template_path = %(project_root)s/templates
|
||||
|
||||
# List of sites allowed cross-origin resource access. If this is empty,
|
||||
# only same-origin requests are allowed.
|
||||
#allowed_cors_origins = http://refstack.net, http://localhost:8080
|
||||
|
||||
# Switch Refstack app into debug mode. Helpful for development. In
|
||||
# debug mode static file will be served by pecan application. Also,
|
||||
# server responses will contain some details with debug information.
|
||||
|
|
|
@ -52,6 +52,11 @@ API_OPTS = [
|
|||
'must contain %(project_root)s variable. Directory with '
|
||||
'template files specified relative the project root.'
|
||||
),
|
||||
cfg.ListOpt('allowed_cors_origins',
|
||||
default=[],
|
||||
help='List of sites allowed cross-site resource access. If '
|
||||
'this is empty, only same-origin requests are allowed.'
|
||||
),
|
||||
cfg.BoolOpt('app_dev_mode',
|
||||
default=False,
|
||||
help='Switch Refstack app into debug mode. Helpful for '
|
||||
|
@ -76,6 +81,7 @@ class JSONErrorHook(pecan.hooks.PecanHook):
|
|||
"""
|
||||
A pecan hook that translates webob HTTP errors into a JSON format.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Hook init."""
|
||||
self.debug = CONF.api.app_dev_mode
|
||||
|
@ -103,6 +109,30 @@ class JSONErrorHook(pecan.hooks.PecanHook):
|
|||
)
|
||||
|
||||
|
||||
class CORSHook(pecan.hooks.PecanHook):
|
||||
"""
|
||||
A pecan hook that handles Cross-Origin Resource Sharing.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Init the hook by getting the allowed origins."""
|
||||
self.allowed_origins = getattr(CONF.api, 'allowed_cors_origins', [])
|
||||
|
||||
def after(self, state):
|
||||
"""Add CORS headers to the response.
|
||||
|
||||
If the request's origin is in the list of allowed origins, add the
|
||||
CORS headers to the response.
|
||||
"""
|
||||
origin = state.request.headers.get('Origin', None)
|
||||
if origin in self.allowed_origins:
|
||||
state.response.headers['Access-Control-Allow-Origin'] = origin
|
||||
state.response.headers['Access-Control-Allow-Methods'] = \
|
||||
'GET, OPTIONS, PUT, POST'
|
||||
state.response.headers['Access-Control-Allow-Headers'] = \
|
||||
'origin, authorization, accept, content-type'
|
||||
|
||||
|
||||
def setup_app(config):
|
||||
"""App factory."""
|
||||
# By default we expect path to oslo config file in environment variable
|
||||
|
@ -133,7 +163,7 @@ def setup_app(config):
|
|||
debug=CONF.api.app_dev_mode,
|
||||
static_root=static_root,
|
||||
template_path=template_path,
|
||||
hooks=[JSONErrorHook(), pecan.hooks.RequestViewerHook(
|
||||
hooks=[JSONErrorHook(), CORSHook(), pecan.hooks.RequestViewerHook(
|
||||
{'items': ['status', 'method', 'controller', 'path', 'body']},
|
||||
headers=False, writer=loggers.WritableLogger(LOG, logging.DEBUG)
|
||||
)]
|
||||
|
|
|
@ -20,6 +20,7 @@ import json
|
|||
import mock
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslotest import base
|
||||
import pecan
|
||||
import webob
|
||||
|
||||
from refstack.api import app
|
||||
|
@ -107,6 +108,66 @@ class JSONErrorHookTestCase(base.BaseTestCase):
|
|||
)
|
||||
|
||||
|
||||
class CORSHookTestCase(base.BaseTestCase):
|
||||
"""
|
||||
Tests for the CORS hook used by the application.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(CORSHookTestCase, self).setUp()
|
||||
self.config_fixture = config_fixture.Config()
|
||||
self.CONF = self.useFixture(self.config_fixture).conf
|
||||
|
||||
def test_allowed_origin(self):
|
||||
"""Test when the origin is in the list of allowed origins."""
|
||||
self.CONF.set_override('allowed_cors_origins', 'test.com', 'api')
|
||||
hook = app.CORSHook()
|
||||
request = pecan.core.Request({})
|
||||
request.headers = {'Origin': 'test.com'}
|
||||
state = pecan.core.RoutingState(request, pecan.core.Response(), None)
|
||||
hook.after(state)
|
||||
|
||||
self.assertIn('Access-Control-Allow-Origin', state.response.headers)
|
||||
allow_origin = state.response.headers['Access-Control-Allow-Origin']
|
||||
self.assertEqual('test.com', allow_origin)
|
||||
|
||||
self.assertIn('Access-Control-Allow-Methods', state.response.headers)
|
||||
allow_methods = state.response.headers['Access-Control-Allow-Methods']
|
||||
self.assertEqual('GET, OPTIONS, PUT, POST', allow_methods)
|
||||
|
||||
self.assertIn('Access-Control-Allow-Headers', state.response.headers)
|
||||
allow_headers = state.response.headers['Access-Control-Allow-Headers']
|
||||
self.assertEqual('origin, authorization, accept, content-type',
|
||||
allow_headers)
|
||||
|
||||
def test_unallowed_origin(self):
|
||||
"""Test when the origin is not in the list of allowed origins."""
|
||||
hook = app.CORSHook()
|
||||
request_headers = {'Origin': 'test.com'}
|
||||
request = pecan.core.Request({})
|
||||
request.headers = request_headers
|
||||
state = pecan.core.RoutingState(request, pecan.core.Response(), None)
|
||||
hook.after(state)
|
||||
self.assertNotIn('Access-Control-Allow-Origin', state.response.headers)
|
||||
self.assertNotIn('Access-Control-Allow-Methods',
|
||||
state.response.headers)
|
||||
self.assertNotIn('Access-Control-Allow-Headers',
|
||||
state.response.headers)
|
||||
|
||||
def test_no_origin_header(self):
|
||||
"""Test when there is no 'Origin' header in the request, in which case,
|
||||
the request is not cross-origin and doesn't need the CORS headers."""
|
||||
hook = app.CORSHook()
|
||||
request = pecan.core.Request({})
|
||||
state = pecan.core.RoutingState(request, pecan.core.Response(), None)
|
||||
hook.after(state)
|
||||
self.assertNotIn('Access-Control-Allow-Origin', state.response.headers)
|
||||
self.assertNotIn('Access-Control-Allow-Methods',
|
||||
state.response.headers)
|
||||
self.assertNotIn('Access-Control-Allow-Headers',
|
||||
state.response.headers)
|
||||
|
||||
|
||||
class SetupAppTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -116,10 +177,11 @@ class SetupAppTestCase(base.BaseTestCase):
|
|||
|
||||
@mock.patch('pecan.hooks')
|
||||
@mock.patch.object(app, 'JSONErrorHook')
|
||||
@mock.patch.object(app, 'CORSHook')
|
||||
@mock.patch('os.path.join')
|
||||
@mock.patch('pecan.make_app')
|
||||
def test_setup_app(self, make_app, os_join,
|
||||
json_error_hook, pecan_hooks):
|
||||
json_error_hook, cors_hook, pecan_hooks):
|
||||
|
||||
self.CONF.set_override('app_dev_mode',
|
||||
True,
|
||||
|
@ -134,6 +196,7 @@ class SetupAppTestCase(base.BaseTestCase):
|
|||
os_join.return_value = 'fake_project_root'
|
||||
|
||||
json_error_hook.return_value = 'json_error_hook'
|
||||
cors_hook.return_value = 'cors_hook'
|
||||
pecan_hooks.RequestViewerHook.return_value = 'request_viewer_hook'
|
||||
pecan_config = mock.Mock()
|
||||
pecan_config.app = {'root': 'fake_pecan_config'}
|
||||
|
@ -149,5 +212,5 @@ class SetupAppTestCase(base.BaseTestCase):
|
|||
debug=True,
|
||||
static_root='fake_static_root',
|
||||
template_path='fake_template_path',
|
||||
hooks=['json_error_hook', 'request_viewer_hook']
|
||||
hooks=['cors_hook', 'json_error_hook', 'request_viewer_hook']
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue