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)
#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.

View File

@ -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)
)]

View File

@ -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']
)