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:
Paul Van Eck 2015-04-17 09:16:44 -07:00
parent 8f972ce693
commit caf52730e7
3 changed files with 100 additions and 3 deletions

View File

@ -116,6 +116,10 @@
# relative the project root. (string value) # relative the project root. (string value)
#template_path = %(project_root)s/templates #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 # Switch Refstack app into debug mode. Helpful for development. In
# debug mode static file will be served by pecan application. Also, # debug mode static file will be served by pecan application. Also,
# server responses will contain some details with debug information. # server responses will contain some details with debug information.

View File

@ -52,6 +52,11 @@ API_OPTS = [
'must contain %(project_root)s variable. Directory with ' 'must contain %(project_root)s variable. Directory with '
'template files specified relative the project root.' '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', cfg.BoolOpt('app_dev_mode',
default=False, default=False,
help='Switch Refstack app into debug mode. Helpful for ' 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. A pecan hook that translates webob HTTP errors into a JSON format.
""" """
def __init__(self): def __init__(self):
"""Hook init.""" """Hook init."""
self.debug = CONF.api.app_dev_mode 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): def setup_app(config):
"""App factory.""" """App factory."""
# By default we expect path to oslo config file in environment variable # 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, debug=CONF.api.app_dev_mode,
static_root=static_root, static_root=static_root,
template_path=template_path, template_path=template_path,
hooks=[JSONErrorHook(), pecan.hooks.RequestViewerHook( hooks=[JSONErrorHook(), CORSHook(), pecan.hooks.RequestViewerHook(
{'items': ['status', 'method', 'controller', 'path', 'body']}, {'items': ['status', 'method', 'controller', 'path', 'body']},
headers=False, writer=loggers.WritableLogger(LOG, logging.DEBUG) headers=False, writer=loggers.WritableLogger(LOG, logging.DEBUG)
)] )]

View File

@ -20,6 +20,7 @@ import json
import mock import mock
from oslo_config import fixture as config_fixture from oslo_config import fixture as config_fixture
from oslotest import base from oslotest import base
import pecan
import webob import webob
from refstack.api import app 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): class SetupAppTestCase(base.BaseTestCase):
def setUp(self): def setUp(self):
@ -116,10 +177,11 @@ class SetupAppTestCase(base.BaseTestCase):
@mock.patch('pecan.hooks') @mock.patch('pecan.hooks')
@mock.patch.object(app, 'JSONErrorHook') @mock.patch.object(app, 'JSONErrorHook')
@mock.patch.object(app, 'CORSHook')
@mock.patch('os.path.join') @mock.patch('os.path.join')
@mock.patch('pecan.make_app') @mock.patch('pecan.make_app')
def test_setup_app(self, make_app, os_join, 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', self.CONF.set_override('app_dev_mode',
True, True,
@ -134,6 +196,7 @@ class SetupAppTestCase(base.BaseTestCase):
os_join.return_value = 'fake_project_root' os_join.return_value = 'fake_project_root'
json_error_hook.return_value = 'json_error_hook' json_error_hook.return_value = 'json_error_hook'
cors_hook.return_value = 'cors_hook'
pecan_hooks.RequestViewerHook.return_value = 'request_viewer_hook' pecan_hooks.RequestViewerHook.return_value = 'request_viewer_hook'
pecan_config = mock.Mock() pecan_config = mock.Mock()
pecan_config.app = {'root': 'fake_pecan_config'} pecan_config.app = {'root': 'fake_pecan_config'}
@ -149,5 +212,5 @@ class SetupAppTestCase(base.BaseTestCase):
debug=True, debug=True,
static_root='fake_static_root', static_root='fake_static_root',
template_path='fake_template_path', template_path='fake_template_path',
hooks=['json_error_hook', 'request_viewer_hook'] hooks=['cors_hook', 'json_error_hook', 'request_viewer_hook']
) )