summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Krotscheck <krotscheck@gmail.com>2015-06-12 14:31:14 -0700
committerMichael Krotscheck <krotscheck@gmail.com>2015-06-30 09:01:48 -0700
commit655de16a871bb0b4768f18a88e9f573fea18ed41 (patch)
tree21b37e890b0780f517702428f7e9314b95c7d3ab
parent0caa7a9e4509ef337925c9586a8b8617cd847158 (diff)
downloadoslo-middleware-655de16a871bb0b4768f18a88e9f573fea18ed41.tar.gz
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
-rw-r--r--doc/source/cors.rst55
-rw-r--r--oslo_middleware/cors.py103
-rw-r--r--oslo_middleware/tests/test_cors.py36
3 files changed, 160 insertions, 34 deletions
diff --git a/doc/source/cors.rst b/doc/source/cors.rst
index b47180c..5763099 100644
--- a/doc/source/cors.rst
+++ b/doc/source/cors.rst
@@ -19,12 +19,34 @@ 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
--------------------
diff --git a/oslo_middleware/cors.py b/oslo_middleware/cors.py
index a0d9322..f178d0c 100644
--- a/oslo_middleware/cors.py
+++ b/oslo_middleware/cors.py
@@ -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])
+
+ 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.
+
+ :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:
+ '''
- # 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
+ if allowed_origin in self.allowed_origins:
+ LOG.warn('Allowed origin [%s] already exists, skipping' % (
+ allowed_origin,))
+ return
- self.allowed_origins[allowed_origin] = conf[section]
+ 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'])
diff --git a/oslo_middleware/tests/test_cors.py b/oslo_middleware/tests/test_cors.py
index 229bb80..d078d14 100644
--- a/oslo_middleware/tests/test_cors.py
+++ b/oslo_middleware/tests/test_cors.py
@@ -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