Support PasteDeploy

This patch removes the oslo_config specific code out of the
middleware's class, and instead provides a factory method by which
this middleware may be constructed. It then also provides a similar
factory method that supports pastedeploy's filter_factory protocol.

Change-Id: I68d9c5439707a624aa24f3dbe7bbfd616b382a6d
This commit is contained in:
Michael Krotscheck 2015-06-12 14:31:14 -07:00
parent 0caa7a9e45
commit 655de16a87
3 changed files with 160 additions and 34 deletions

View File

@ -18,13 +18,35 @@ Quickstart
----------
First, include the middleware in your application::
from oslo_middleware import cors
app = cors.CORS(your_wsgi_application)
Secondly, add as many allowed origins as you would like::
app.add_origin(allowed_origin='https://website.example.com:443',
allow_credentials=True,
max_age=3600,
allow_methods=['GET','PUT','POST','DELETE'],
allow_headers=['X-Custom-Header'],
expose_headers=['X-Custom-Header'])
# ... add more origins here.
Configuration for oslo_config
-----------------------------
A factory method has been provided to simplify configuration of your CORS
domain, using oslo_config::
from oslo_middleware import cors
from oslo_config import cfg
app = cors.CORS(your_wsgi_application, cfg.CONF)
Secondly, add a global [cors] configuration block to the configuration file
read by oslo.config::
In your application's config file, then include a default configuration block
something like this::
[cors]
allowed_origin=https://website.example.com:443
@ -33,15 +55,11 @@ read by oslo.config::
allow_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header
expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header
Advanced Configuration
----------------------
CORS Middleware permits you to define multiple `allowed_origin`'s, and to
selectively override the global configuration for each. To accomplish this,
first follow the setup instructions in the Quickstart above.
Then, create an new configuration group for each domain that you'd like to
extend. Each of these configuration groups must be named `[cors.something]`,
with each name being unique. The purpose of the suffix to `cors.` is
This middleware permits you to define multiple `allowed_origin`'s. To express
this in your configuration file, first begin with a `[cors]` group as above,
into which you place your default configuration values. Then add as many
additional configuration groups as necessary, naming them `[cors.something]`
(each name must be unique). The purpose of the suffix to `cors.` is
legibility, we recommend using a reasonable human-readable string::
[cors.ironic_webclient]
@ -61,6 +79,21 @@ legibility, we recommend using a reasonable human-readable string::
allow_methods=GET
Configuration for pastedeploy
-----------------------------
If your application is using pastedeploy, the following configuration block
will add CORS support. To add multiple domains, simply add another filter.::
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
allowed_origin=https://website.example.com:443
max_age=3600
allow_methods=GET,POST,PUT,DELETE
allow_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header
expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header
Module Documentation
--------------------

View File

@ -54,6 +54,38 @@ CORS_OPTS = [
]
def filter_factory(global_conf,
allowed_origin,
allow_credentials=True,
expose_headers=None,
max_age=None,
allow_methods=None,
allow_headers=None):
'''Factory to support paste.deploy
:param global_conf: The paste.ini global configuration object (not used).
:param allowed_origin: Protocol, host, and port for the allowed origin.
:param allow_credentials: Whether to permit credentials.
:param expose_headers: A list of headers to expose.
:param max_age: Maximum cache duration.
:param allow_methods: List of HTTP methods to permit.
:param allow_headers: List of HTTP headers to permit from the client.
:return:
'''
def filter(app):
cors_app = CORS(app)
cors_app.add_origin(allowed_origin=allowed_origin,
allow_credentials=allow_credentials,
expose_headers=expose_headers,
max_age=max_age,
allow_methods=allow_methods,
allow_headers=allow_headers)
return cors_app
return filter
class CORS(base.Middleware):
"""CORS Middleware.
@ -72,13 +104,21 @@ class CORS(base.Middleware):
'Pragma'
]
def __init__(self, application, conf):
def __init__(self, application, conf=None):
super(CORS, self).__init__(application)
# Begin constructing our configuration hash.
self.allowed_origins = {}
# Sanity check. Do we have an oslo.config? If so, load it. Else, assume
# that we'll use add_config.
if conf:
self._init_from_oslo(conf)
def _init_from_oslo(self, conf):
'''Initialize this middleware from an oslo.config instance.'''
# First, check the configuration and register global options.
if not conf or not isinstance(conf, cfg.ConfigOpts):
raise ValueError("This middleware requires a configuration of"
" type oslo_config.ConfigOpts.")
conf.register_opts(CORS_OPTS, 'cors')
# Clone our original CORS_OPTS, and set the defaults to whatever is
@ -93,13 +133,10 @@ class CORS(base.Middleware):
allow_methods=conf.cors.allow_methods,
allow_headers=conf.cors.allow_headers)
# Begin constructing our configuration hash.
self.allowed_origins = {}
# If the default configuration contains an allowed_origin, don't
# forget to register that.
if conf.cors.allowed_origin:
self.allowed_origins[conf.cors.allowed_origin] = conf.cors
self.add_origin(**conf.cors)
# Iterate through all the loaded config sections, looking for ones
# prefixed with 'cors.'
@ -107,15 +144,34 @@ class CORS(base.Middleware):
if section.startswith('cors.'):
# Register with the preconstructed defaults
conf.register_opts(subgroup_opts, section)
self.add_origin(**conf[section])
# Make sure that allowed_origin is available. Otherwise skip.
allowed_origin = conf[section].allowed_origin
if not allowed_origin:
LOG.warn('Config section [%s] does not contain'
' \'allowed_origin\', skipping.' % (section,))
continue
def add_origin(self, allowed_origin, allow_credentials=True,
expose_headers=None, max_age=None, allow_methods=None,
allow_headers=None):
'''Add another origin to this filter.
self.allowed_origins[allowed_origin] = conf[section]
:param allowed_origin: Protocol, host, and port for the allowed origin.
:param allow_credentials: Whether to permit credentials.
:param expose_headers: A list of headers to expose.
:param max_age: Maximum cache duration.
:param allow_methods: List of HTTP methods to permit.
:param allow_headers: List of HTTP headers to permit from the client.
:return:
'''
if allowed_origin in self.allowed_origins:
LOG.warn('Allowed origin [%s] already exists, skipping' % (
allowed_origin,))
return
self.allowed_origins[allowed_origin] = {
'allow_credentials': allow_credentials,
'expose_headers': expose_headers,
'max_age': max_age,
'allow_methods': allow_methods,
'allow_headers': allow_headers
}
def process_response(self, response, request=None):
'''Check for CORS headers, and decorate if necessary.
@ -198,14 +254,15 @@ class CORS(base.Middleware):
return response
# Compare request method to permitted methods (Section 6.2.5)
if request_method not in cors_config.allow_methods:
if request_method not in cors_config['allow_methods']:
return response
# Compare request headers to permitted headers, case-insensitively.
# (Section 6.2.6)
for requested_header in request_headers:
upper_header = requested_header.upper()
permitted_headers = cors_config.allow_headers + self.simple_headers
permitted_headers = (cors_config['allow_headers'] +
self.simple_headers)
if upper_header not in (header.upper() for header in
permitted_headers):
return response
@ -215,13 +272,13 @@ class CORS(base.Middleware):
response.headers['Access-Control-Allow-Origin'] = origin
# Does this CORS configuration permit credentials? (Section 6.2.7)
if cors_config.allow_credentials:
if cors_config['allow_credentials']:
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Attach Access-Control-Max-Age if appropriate. (Section 6.2.8)
if 'max_age' in cors_config and cors_config.max_age:
if 'max_age' in cors_config and cors_config['max_age']:
response.headers['Access-Control-Max-Age'] = \
str(cors_config.max_age)
str(cors_config['max_age'])
# Attach Access-Control-Allow-Methods. (Section 6.2.9)
response.headers['Access-Control-Allow-Methods'] = request_method
@ -257,10 +314,10 @@ class CORS(base.Middleware):
response.headers['Access-Control-Allow-Origin'] = origin
# Does this CORS configuration permit credentials? (Section 6.1.3)
if cors_config.allow_credentials:
if cors_config['allow_credentials']:
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Attach the exposed headers and exit. (Section 6.1.4)
if cors_config.expose_headers:
if cors_config['expose_headers']:
response.headers['Access-Control-Expose-Headers'] = \
','.join(cors_config.expose_headers)
','.join(cors_config['expose_headers'])

View File

@ -111,6 +111,42 @@ class CORSTestBase(test_base.BaseTestCase):
self.assertNotIn(header, response.headers)
class CORSTestFilterFactory(test_base.BaseTestCase):
"""Test the CORS filter_factory method."""
def test_filter_factory(self):
# Test a valid filter.
filter = cors.filter_factory(None,
allowed_origin='http://valid.example.com',
allow_credentials='False',
max_age='',
expose_headers='',
allow_methods='GET',
allow_headers='')
application = filter(test_application)
self.assertIn('http://valid.example.com', application.allowed_origins)
config = application.allowed_origins['http://valid.example.com']
self.assertEqual('False', config['allow_credentials'])
self.assertEqual('', config['max_age'])
self.assertEqual('', config['expose_headers'])
self.assertEqual('GET', config['allow_methods'])
self.assertEqual('', config['allow_headers'])
def test_no_origin_fail(self):
'''Assert that a filter factory with no allowed_origin fails.'''
self.assertRaises(TypeError,
cors.filter_factory,
global_conf=None,
# allowed_origin=None, # Expected value.
allow_credentials='False',
max_age='',
expose_headers='',
allow_methods='GET',
allow_headers='')
class CORSRegularRequestTest(CORSTestBase):
"""CORS Specification Section 6.1