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:
parent
0caa7a9e45
commit
655de16a87
|
@ -18,13 +18,35 @@ Quickstart
|
||||||
----------
|
----------
|
||||||
First, include the middleware in your application::
|
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_middleware import cors
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
app = cors.CORS(your_wsgi_application, cfg.CONF)
|
app = cors.CORS(your_wsgi_application, cfg.CONF)
|
||||||
|
|
||||||
Secondly, add a global [cors] configuration block to the configuration file
|
In your application's config file, then include a default configuration block
|
||||||
read by oslo.config::
|
something like this::
|
||||||
|
|
||||||
[cors]
|
[cors]
|
||||||
allowed_origin=https://website.example.com:443
|
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
|
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
|
expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header
|
||||||
|
|
||||||
Advanced Configuration
|
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,
|
||||||
CORS Middleware permits you to define multiple `allowed_origin`'s, and to
|
into which you place your default configuration values. Then add as many
|
||||||
selectively override the global configuration for each. To accomplish this,
|
additional configuration groups as necessary, naming them `[cors.something]`
|
||||||
first follow the setup instructions in the Quickstart above.
|
(each name must be unique). The purpose of the suffix to `cors.` is
|
||||||
|
|
||||||
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
|
|
||||||
legibility, we recommend using a reasonable human-readable string::
|
legibility, we recommend using a reasonable human-readable string::
|
||||||
|
|
||||||
[cors.ironic_webclient]
|
[cors.ironic_webclient]
|
||||||
|
@ -61,6 +79,21 @@ legibility, we recommend using a reasonable human-readable string::
|
||||||
allow_methods=GET
|
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
|
Module Documentation
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|
|
@ -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):
|
class CORS(base.Middleware):
|
||||||
"""CORS Middleware.
|
"""CORS Middleware.
|
||||||
|
|
||||||
|
@ -72,13 +104,21 @@ class CORS(base.Middleware):
|
||||||
'Pragma'
|
'Pragma'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, application, conf):
|
def __init__(self, application, conf=None):
|
||||||
super(CORS, self).__init__(application)
|
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.
|
# 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')
|
conf.register_opts(CORS_OPTS, 'cors')
|
||||||
|
|
||||||
# Clone our original CORS_OPTS, and set the defaults to whatever is
|
# 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_methods=conf.cors.allow_methods,
|
||||||
allow_headers=conf.cors.allow_headers)
|
allow_headers=conf.cors.allow_headers)
|
||||||
|
|
||||||
# Begin constructing our configuration hash.
|
|
||||||
self.allowed_origins = {}
|
|
||||||
|
|
||||||
# If the default configuration contains an allowed_origin, don't
|
# If the default configuration contains an allowed_origin, don't
|
||||||
# forget to register that.
|
# forget to register that.
|
||||||
if conf.cors.allowed_origin:
|
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
|
# Iterate through all the loaded config sections, looking for ones
|
||||||
# prefixed with 'cors.'
|
# prefixed with 'cors.'
|
||||||
|
@ -107,15 +144,34 @@ class CORS(base.Middleware):
|
||||||
if section.startswith('cors.'):
|
if section.startswith('cors.'):
|
||||||
# Register with the preconstructed defaults
|
# Register with the preconstructed defaults
|
||||||
conf.register_opts(subgroup_opts, section)
|
conf.register_opts(subgroup_opts, section)
|
||||||
|
self.add_origin(**conf[section])
|
||||||
|
|
||||||
# Make sure that allowed_origin is available. Otherwise skip.
|
def add_origin(self, allowed_origin, allow_credentials=True,
|
||||||
allowed_origin = conf[section].allowed_origin
|
expose_headers=None, max_age=None, allow_methods=None,
|
||||||
if not allowed_origin:
|
allow_headers=None):
|
||||||
LOG.warn('Config section [%s] does not contain'
|
'''Add another origin to this filter.
|
||||||
' \'allowed_origin\', skipping.' % (section,))
|
|
||||||
continue
|
|
||||||
|
|
||||||
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):
|
def process_response(self, response, request=None):
|
||||||
'''Check for CORS headers, and decorate if necessary.
|
'''Check for CORS headers, and decorate if necessary.
|
||||||
|
@ -198,14 +254,15 @@ class CORS(base.Middleware):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Compare request method to permitted methods (Section 6.2.5)
|
# 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
|
return response
|
||||||
|
|
||||||
# Compare request headers to permitted headers, case-insensitively.
|
# Compare request headers to permitted headers, case-insensitively.
|
||||||
# (Section 6.2.6)
|
# (Section 6.2.6)
|
||||||
for requested_header in request_headers:
|
for requested_header in request_headers:
|
||||||
upper_header = requested_header.upper()
|
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
|
if upper_header not in (header.upper() for header in
|
||||||
permitted_headers):
|
permitted_headers):
|
||||||
return response
|
return response
|
||||||
|
@ -215,13 +272,13 @@ class CORS(base.Middleware):
|
||||||
response.headers['Access-Control-Allow-Origin'] = origin
|
response.headers['Access-Control-Allow-Origin'] = origin
|
||||||
|
|
||||||
# Does this CORS configuration permit credentials? (Section 6.2.7)
|
# 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'
|
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
|
|
||||||
# Attach Access-Control-Max-Age if appropriate. (Section 6.2.8)
|
# 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'] = \
|
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)
|
# Attach Access-Control-Allow-Methods. (Section 6.2.9)
|
||||||
response.headers['Access-Control-Allow-Methods'] = request_method
|
response.headers['Access-Control-Allow-Methods'] = request_method
|
||||||
|
@ -257,10 +314,10 @@ class CORS(base.Middleware):
|
||||||
response.headers['Access-Control-Allow-Origin'] = origin
|
response.headers['Access-Control-Allow-Origin'] = origin
|
||||||
|
|
||||||
# Does this CORS configuration permit credentials? (Section 6.1.3)
|
# 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'
|
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
|
|
||||||
# Attach the exposed headers and exit. (Section 6.1.4)
|
# 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'] = \
|
response.headers['Access-Control-Expose-Headers'] = \
|
||||||
','.join(cors_config.expose_headers)
|
','.join(cors_config['expose_headers'])
|
||||||
|
|
|
@ -111,6 +111,42 @@ class CORSTestBase(test_base.BaseTestCase):
|
||||||
self.assertNotIn(header, response.headers)
|
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):
|
class CORSRegularRequestTest(CORSTestBase):
|
||||||
"""CORS Specification Section 6.1
|
"""CORS Specification Section 6.1
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue