Switched StrOpt to ListOpt in CORS allowed_origins

This patch switches the 'allowed_origin' CORS configuration option
from a single string to an array of strings. This will let you
configure multiple domains simultaneously with the same options,
without having to add additional configuration blocks.

By doing this, pastedeploy users will no longer have to configure
mulitple filters if they wish to grant access to more than one
domain.

Change-Id: Ie2e57b76717604f701daa16ebf8ffa8c06835e3c
This commit is contained in:
Michael Krotscheck 2015-10-19 05:26:06 -07:00
parent 4e7bb27895
commit c4957606cb
3 changed files with 81 additions and 38 deletions

View File

@ -49,18 +49,19 @@ In your application's config file, then include a default configuration block
something like this::
[cors]
allowed_origin=https://website.example.com:443
allowed_origin=https://website.example.com:443,https://website2.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
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::
This middleware permits you to override the rules for 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]
# CORS Configuration for a hypothetical ironic webclient, which overrides
@ -94,11 +95,11 @@ 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.::
will add CORS support.::
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
allowed_origin=https://website.example.com:443
allowed_origin=https://website.example.com:443,https://website2.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

View File

@ -25,10 +25,10 @@ import webob.response
LOG = logging.getLogger(__name__)
CORS_OPTS = [
cfg.StrOpt('allowed_origin',
default=None,
help='Indicate whether this resource may be shared with the '
'domain received in the requests "origin" header.'),
cfg.ListOpt('allowed_origin',
default=None,
help='Indicate whether this resource may be shared with the '
'domain received in the requests "origin" header.'),
cfg.BoolOpt('allow_credentials',
default=True,
help='Indicate that the actual request can include user '
@ -180,19 +180,20 @@ class CORS(base.ConfigurableMiddleware):
:param allow_headers: List of HTTP headers to permit from the client.
:return:
'''
for origin in allowed_origin:
if allowed_origin in self.allowed_origins:
LOG.warn('Allowed origin [%s] already exists, skipping' % (
allowed_origin,))
return
if origin in self.allowed_origins:
LOG.warn('Allowed origin [%s] already exists, skipping' % (
allowed_origin,))
continue
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
}
self.allowed_origins[origin] = {
'allow_credentials': allow_credentials,
'expose_headers': expose_headers,
'max_age': max_age,
'allow_methods': allow_methods,
'allow_headers': allow_headers
}
def set_latent(self, allow_headers=None, allow_methods=None,
expose_headers=None):

View File

@ -136,6 +136,18 @@ class CORSTestFilterFactory(test_base.BaseTestCase):
self.assertEqual(['GET'], config['allow_methods'])
self.assertEqual([], config['allow_headers'])
def test_filter_factory_multiorigin(self):
self.useFixture(fixture.Config()).conf([])
# Test a valid filter.
filter = cors.filter_factory(None,
allowed_origin='http://valid.example.com,'
'http://other.example.com')
application = filter(test_application)
self.assertIn('http://valid.example.com', application.allowed_origins)
self.assertIn('http://other.example.com', application.allowed_origins)
def test_no_origin_fail(self):
'''Assert that a filter factory with no allowed_origin fails.'''
self.assertRaises(TypeError,
@ -220,6 +232,10 @@ class CORSRegularRequestTest(CORSTestBase):
allowed_origin='http://all.example.com',
allow_methods='GET,PUT,POST,DELETE,HEAD')
config.load_raw_values(group='cors.duplicate',
allowed_origin='http://domain1.example.com,'
'http://domain2.example.com')
# Now that the config is set up, create our application.
self.application = cors.CORS(test_application, cfg.CONF)
@ -228,7 +244,7 @@ class CORSRegularRequestTest(CORSTestBase):
# Confirm global configuration
gc = cfg.CONF.cors
self.assertEqual(gc.allowed_origin, 'http://valid.example.com')
self.assertEqual(gc.allowed_origin, ['http://valid.example.com'])
self.assertEqual(gc.allow_credentials, False)
self.assertEqual(gc.expose_headers, [])
self.assertEqual(gc.max_age, None)
@ -237,7 +253,7 @@ class CORSRegularRequestTest(CORSTestBase):
# Confirm credentials overrides.
cc = cfg.CONF['cors.credentials']
self.assertEqual(cc.allowed_origin, 'http://creds.example.com')
self.assertEqual(cc.allowed_origin, ['http://creds.example.com'])
self.assertEqual(cc.allow_credentials, True)
self.assertEqual(cc.expose_headers, gc.expose_headers)
self.assertEqual(cc.max_age, gc.max_age)
@ -246,7 +262,7 @@ class CORSRegularRequestTest(CORSTestBase):
# Confirm exposed-headers overrides.
ec = cfg.CONF['cors.exposed-headers']
self.assertEqual(ec.allowed_origin, 'http://headers.example.com')
self.assertEqual(ec.allowed_origin, ['http://headers.example.com'])
self.assertEqual(ec.allow_credentials, gc.allow_credentials)
self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2'])
self.assertEqual(ec.max_age, gc.max_age)
@ -255,7 +271,7 @@ class CORSRegularRequestTest(CORSTestBase):
# Confirm cached overrides.
chc = cfg.CONF['cors.cached']
self.assertEqual(chc.allowed_origin, 'http://cached.example.com')
self.assertEqual(chc.allowed_origin, ['http://cached.example.com'])
self.assertEqual(chc.allow_credentials, gc.allow_credentials)
self.assertEqual(chc.expose_headers, gc.expose_headers)
self.assertEqual(chc.max_age, 3600)
@ -264,7 +280,7 @@ class CORSRegularRequestTest(CORSTestBase):
# Confirm get-only overrides.
goc = cfg.CONF['cors.get-only']
self.assertEqual(goc.allowed_origin, 'http://get.example.com')
self.assertEqual(goc.allowed_origin, ['http://get.example.com'])
self.assertEqual(goc.allow_credentials, gc.allow_credentials)
self.assertEqual(goc.expose_headers, gc.expose_headers)
self.assertEqual(goc.max_age, gc.max_age)
@ -273,7 +289,7 @@ class CORSRegularRequestTest(CORSTestBase):
# Confirm all-methods overrides.
ac = cfg.CONF['cors.all-methods']
self.assertEqual(ac.allowed_origin, 'http://all.example.com')
self.assertEqual(ac.allowed_origin, ['http://all.example.com'])
self.assertEqual(ac.allow_credentials, gc.allow_credentials)
self.assertEqual(ac.expose_headers, gc.expose_headers)
self.assertEqual(ac.max_age, gc.max_age)
@ -281,6 +297,16 @@ class CORSRegularRequestTest(CORSTestBase):
['GET', 'PUT', 'POST', 'DELETE', 'HEAD'])
self.assertEqual(ac.allow_headers, gc.allow_headers)
# Confirm duplicate domains.
ac = cfg.CONF['cors.duplicate']
self.assertEqual(ac.allowed_origin, ['http://domain1.example.com',
'http://domain2.example.com'])
self.assertEqual(ac.allow_credentials, gc.allow_credentials)
self.assertEqual(ac.expose_headers, gc.expose_headers)
self.assertEqual(ac.max_age, gc.max_age)
self.assertEqual(ac.allow_methods, gc.allow_methods)
self.assertEqual(ac.allow_headers, gc.allow_headers)
def test_no_origin_header(self):
"""CORS Specification Section 6.1.1
@ -352,6 +378,21 @@ class CORSRegularRequestTest(CORSTestBase):
allow_credentials=None,
expose_headers=None)
# Test valid header from list of duplicates.
for method in self.methods:
request = webob.Request.blank('/')
request.method = method
request.headers['Origin'] = 'http://domain2.example.com'
response = request.get_response(self.application)
self.assertCORSResponse(response,
status='200 OK',
allow_origin='http://domain2.example.com',
max_age=None,
allow_methods=None,
allow_headers=None,
allow_credentials=None,
expose_headers=None)
def test_supports_credentials(self):
"""CORS Specification Section 6.1.3
@ -488,7 +529,7 @@ class CORSPreflightRequestTest(CORSTestBase):
# Confirm global configuration
gc = cfg.CONF.cors
self.assertEqual(gc.allowed_origin, 'http://valid.example.com')
self.assertEqual(gc.allowed_origin, ['http://valid.example.com'])
self.assertEqual(gc.allow_credentials, False)
self.assertEqual(gc.expose_headers, [])
self.assertEqual(gc.max_age, None)
@ -497,7 +538,7 @@ class CORSPreflightRequestTest(CORSTestBase):
# Confirm credentials overrides.
cc = cfg.CONF['cors.credentials']
self.assertEqual(cc.allowed_origin, 'http://creds.example.com')
self.assertEqual(cc.allowed_origin, ['http://creds.example.com'])
self.assertEqual(cc.allow_credentials, True)
self.assertEqual(cc.expose_headers, gc.expose_headers)
self.assertEqual(cc.max_age, gc.max_age)
@ -506,7 +547,7 @@ class CORSPreflightRequestTest(CORSTestBase):
# Confirm exposed-headers overrides.
ec = cfg.CONF['cors.exposed-headers']
self.assertEqual(ec.allowed_origin, 'http://headers.example.com')
self.assertEqual(ec.allowed_origin, ['http://headers.example.com'])
self.assertEqual(ec.allow_credentials, gc.allow_credentials)
self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2'])
self.assertEqual(ec.max_age, gc.max_age)
@ -515,7 +556,7 @@ class CORSPreflightRequestTest(CORSTestBase):
# Confirm cached overrides.
chc = cfg.CONF['cors.cached']
self.assertEqual(chc.allowed_origin, 'http://cached.example.com')
self.assertEqual(chc.allowed_origin, ['http://cached.example.com'])
self.assertEqual(chc.allow_credentials, gc.allow_credentials)
self.assertEqual(chc.expose_headers, gc.expose_headers)
self.assertEqual(chc.max_age, 3600)
@ -524,7 +565,7 @@ class CORSPreflightRequestTest(CORSTestBase):
# Confirm get-only overrides.
goc = cfg.CONF['cors.get-only']
self.assertEqual(goc.allowed_origin, 'http://get.example.com')
self.assertEqual(goc.allowed_origin, ['http://get.example.com'])
self.assertEqual(goc.allow_credentials, gc.allow_credentials)
self.assertEqual(goc.expose_headers, gc.expose_headers)
self.assertEqual(goc.max_age, gc.max_age)
@ -533,7 +574,7 @@ class CORSPreflightRequestTest(CORSTestBase):
# Confirm all-methods overrides.
ac = cfg.CONF['cors.all-methods']
self.assertEqual(ac.allowed_origin, 'http://all.example.com')
self.assertEqual(ac.allowed_origin, ['http://all.example.com'])
self.assertEqual(ac.allow_credentials, gc.allow_credentials)
self.assertEqual(ac.expose_headers, gc.expose_headers)
self.assertEqual(ac.max_age, gc.max_age)
@ -1006,7 +1047,7 @@ class CORSTestWildcard(CORSTestBase):
# Confirm global configuration
gc = cfg.CONF.cors
self.assertEqual(gc.allowed_origin, 'http://default.example.com')
self.assertEqual(gc.allowed_origin, ['http://default.example.com'])
self.assertEqual(gc.allow_credentials, True)
self.assertEqual(gc.expose_headers, [])
self.assertEqual(gc.max_age, None)
@ -1016,7 +1057,7 @@ class CORSTestWildcard(CORSTestBase):
# Confirm all-methods overrides.
ac = cfg.CONF['cors.wildcard']
self.assertEqual(ac.allowed_origin, '*')
self.assertEqual(ac.allowed_origin, ['*'])
self.assertEqual(gc.allow_credentials, True)
self.assertEqual(ac.expose_headers, gc.expose_headers)
self.assertEqual(ac.max_age, gc.max_age)