CORS support for StoryBoard API
Added new middleware that intercepts and decorates all requests in accordance to the Cross Origin Resource Sharing W3C specification found at http://www.w3.org/TR/cors/. Permitted origins and cache max age are configurable, however headers and methods are hard-coded since those are application-specific. The notification hook was also updated to only trigger on POST, PUT, and DELETE. Motivation for this change is to allow js-draft builds of the storyboard webclient to break the browser sandbox, and become fully operational by accessing the production storyboard api. Reviewers interested in seeing a functioning webclient for UI review will no longer have to download and run their own client. Patch to make the webclient build support a configurable API backend is forthcoming. Change-Id: I7a825820e4edf48cd9552b2c1c656bc7e664a25a
This commit is contained in:
parent
cfaebb4999
commit
2da943a2f4
@ -50,6 +50,15 @@ lock_path = $state_path/lock
|
||||
# and subscriptions.
|
||||
# enable_notifications = True
|
||||
|
||||
[cors]
|
||||
# W3C CORS configuration. For more information, see http://www.w3.org/TR/cors/
|
||||
|
||||
# List of permitted CORS domains.
|
||||
# allowed_origins = https://storyboard.openstack.org, http://localhost:9000
|
||||
|
||||
# CORS browser options cache max age (in seconds)
|
||||
# max_age=3600
|
||||
|
||||
[notifications]
|
||||
|
||||
# Host of the rabbitmq server.
|
||||
|
@ -22,6 +22,7 @@ from wsgiref import simple_server
|
||||
from storyboard.api.auth.token_storage import impls as storage_impls
|
||||
from storyboard.api.auth.token_storage import storage
|
||||
from storyboard.api import config as api_config
|
||||
from storyboard.api.middleware.cors_middleware import CORSMiddleware
|
||||
from storyboard.api.middleware import token_middleware
|
||||
from storyboard.api.middleware import user_id_hook
|
||||
from storyboard.api.v1.search import impls as search_engine_impls
|
||||
@ -45,7 +46,16 @@ API_OPTS = [
|
||||
default=False,
|
||||
help='Enable Notifications')
|
||||
]
|
||||
CORS_OPTS = [
|
||||
cfg.ListOpt('allowed_origins',
|
||||
default=None,
|
||||
help='List of permitted CORS origins.'),
|
||||
cfg.IntOpt('max_age',
|
||||
default=3600,
|
||||
help='Maximum cache age of CORS preflight requests.')
|
||||
]
|
||||
CONF.register_opts(API_OPTS)
|
||||
CONF.register_opts(CORS_OPTS, 'cors')
|
||||
|
||||
|
||||
def get_pecan_config():
|
||||
@ -95,6 +105,17 @@ def setup_app(pecan_config=None):
|
||||
|
||||
app = token_middleware.AuthTokenMiddleware(app)
|
||||
|
||||
# Setup CORS
|
||||
if CONF.cors.allowed_origins:
|
||||
app = CORSMiddleware(app,
|
||||
allowed_origins=CONF.cors.allowed_origins,
|
||||
allowed_methods=['GET', 'POST', 'PUT', 'DELETE',
|
||||
'OPTIONS'],
|
||||
allowed_headers=['origin', 'authorization',
|
||||
'accept', 'x-total', 'x-limit',
|
||||
'x-marker', 'x-client'],
|
||||
max_age=CONF.cors.max_age)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
113
storyboard/api/middleware/cors_middleware.py
Normal file
113
storyboard/api/middleware/cors_middleware.py
Normal file
@ -0,0 +1,113 @@
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Default allowed headers
|
||||
ALLOWED_HEADERS = [
|
||||
'origin',
|
||||
'authorization',
|
||||
'accept'
|
||||
]
|
||||
# Default allowed methods
|
||||
ALLOWED_METHODS = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'OPTIONS'
|
||||
]
|
||||
|
||||
|
||||
class CORSMiddleware(object):
|
||||
"""CORS Middleware.
|
||||
|
||||
By providing a list of allowed origins, methods, headers, and a max-age,
|
||||
this middleware will detect and apply the appropriate CORS headers so
|
||||
that your web application may elegantly overcome the browser's
|
||||
same-origin sandbox.
|
||||
|
||||
For more information, see http://www.w3.org/TR/cors/
|
||||
"""
|
||||
|
||||
def __init__(self, app, allowed_origins=None, allowed_methods=None,
|
||||
allowed_headers=None, max_age=3600):
|
||||
"""Create a new instance of the CORS middleware.
|
||||
|
||||
:param app: The application that is being wrapped.
|
||||
:param allowed_origins: A list of allowed origins, as provided by the
|
||||
'Origin:' Http header. Must include protocol, host, and port.
|
||||
:param allowed_methods: A list of allowed HTTP methods.
|
||||
:param allowed_headers: A list of allowed HTTP headers.
|
||||
:param max_age: A maximum CORS cache age in seconds.
|
||||
:return: A new middleware instance.
|
||||
"""
|
||||
|
||||
# Wrapped app (or other middleware)
|
||||
self.app = app
|
||||
|
||||
# Allowed origins
|
||||
self.allowed_origins = allowed_origins or []
|
||||
|
||||
# List of allowed headers.
|
||||
self.allowed_headers = ','.join(allowed_headers or ALLOWED_HEADERS)
|
||||
|
||||
# List of allowed methods.
|
||||
self.allowed_methods = ','.join(allowed_methods or ALLOWED_METHODS)
|
||||
|
||||
# Cache age.
|
||||
self.max_age = str(max_age)
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
"""Serve an application request.
|
||||
|
||||
:param env: Application environment parameters.
|
||||
:param start_response: Wrapper method that starts the response.
|
||||
:return:
|
||||
"""
|
||||
origin = env['HTTP_ORIGIN'] if 'HTTP_ORIGIN' in env else ''
|
||||
method = env['REQUEST_METHOD'] if 'REQUEST_METHOD' in env else ''
|
||||
|
||||
def replacement_start_response(status, headers, exc_info=None):
|
||||
"""Overrides the default response to attach CORS headers.
|
||||
"""
|
||||
|
||||
# Decorate the headers
|
||||
headers.append(('Access-Control-Allow-Origin',
|
||||
origin))
|
||||
headers.append(('Access-Control-Allow-Methods',
|
||||
self.allowed_methods))
|
||||
headers.append(('Access-Control-Expose-Headers',
|
||||
self.allowed_headers))
|
||||
headers.append(('Access-Control-Allow-Headers',
|
||||
self.allowed_headers))
|
||||
headers.append(('Access-Control-Max-Age',
|
||||
self.max_age))
|
||||
|
||||
return start_response(status, headers, exc_info)
|
||||
|
||||
# Does this request match one of our origin domains?
|
||||
if origin in self.allowed_origins:
|
||||
|
||||
# Is this an OPTIONS request?
|
||||
if method == 'OPTIONS':
|
||||
options_headers = [('Content-Length', '0')]
|
||||
replacement_start_response('204 No Content', options_headers)
|
||||
return ''
|
||||
else:
|
||||
# Handle the request.
|
||||
return self.app(env, replacement_start_response)
|
||||
else:
|
||||
# This is not a request for a permitted CORS domain. Return
|
||||
# the response without the appropriate headers and let the browser
|
||||
# figure out the details.
|
||||
return self.app(env, start_response)
|
@ -27,7 +27,7 @@ class NotificationHook(hooks.PecanHook):
|
||||
|
||||
def after(self, state):
|
||||
# Ignore get methods, we only care about changes.
|
||||
if state.request.method == 'GET':
|
||||
if state.request.method not in ['POST', 'PUT', 'DELETE']:
|
||||
return
|
||||
|
||||
request = state.request
|
||||
|
Loading…
Reference in New Issue
Block a user