Added CORS support to CloudKitty

This adds the CORS support middleware to CloudKitty, allowing a deployer
to optionally configure rules under which a javascript client may
break the single-origin policy and access the API directly.

For CloudKitty, we used the same gabbi fixtures and harnesses created for
ceilometer. A custom gabbi-paste.ini was created so that the API tests run
against the full wsgi application, and appropriate fixtures and tests were
created to correctly simulate the configuration state.

Hooks to ensure that cloudkitty's required HTTP headers are represented
both in the runtime defaults, and in the automatically generated config
file, have also been added.

OpenStack CrossProject Spec:
   http://specs.openstack.org/openstack/openstack-specs/specs/cors-support.html
Oslo_Middleware Docs:
   http://docs.openstack.org/developer/oslo.middleware/cors.html
OpenStack Cloud Admin Guide:
   http://docs.openstack.org/admin-guide-cloud/cross_project_cors.html
DocImpact: Add link to CORS configuration in admin cloud guide.

Change-Id: I3ef96ca6c78c5e369fb09425871d0a57bf15ad8a
This commit is contained in:
Michael Krotscheck 2015-12-09 16:41:11 -08:00 committed by Stéphane Albert
parent 670459e236
commit 98a13bc212
9 changed files with 199 additions and 2 deletions

View File

@ -0,0 +1,46 @@
# Copyright 2016 Hewlett Packard Enterprise Development Corporation, LP
#
# 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.
#
from oslo_config import cfg
from oslo_middleware import cors
def set_config_defaults():
"""This method updates all configuration default values."""
set_cors_middleware_defaults()
def set_cors_middleware_defaults():
"""Update default configuration options for oslo.middleware."""
# CORS Defaults
# TODO(krotscheck): Update with https://review.openstack.org/#/c/285368/
cfg.set_defaults(cors.CORS_OPTS,
allow_headers=['X-Auth-Token',
'X-Identity-Status',
'X-Roles',
'X-Service-Catalog',
'X-User-Id',
'X-Tenant-Id',
'X-OpenStack-Request-ID'],
expose_headers=['X-Auth-Token',
'X-Subject-Token',
'X-Service-Token',
'X-OpenStack-Request-ID'],
allow_methods=['GET',
'PUT',
'POST',
'DELETE',
'PATCH']
)

View File

@ -21,6 +21,8 @@ import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from cloudkitty.common import defaults
service_opts = [ service_opts = [
cfg.StrOpt('host', cfg.StrOpt('host',
@ -38,5 +40,6 @@ cfg.CONF.register_opts(service_opts)
def prepare_service(): def prepare_service():
logging.register_options(cfg.CONF) logging.register_options(cfg.CONF)
cfg.CONF(sys.argv[1:], project='cloudkitty') cfg.CONF(sys.argv[1:], project='cloudkitty')
defaults.set_config_defaults()
logging.setup(cfg.CONF, 'cloudkitty') logging.setup(cfg.CONF, 'cloudkitty')

View File

@ -193,6 +193,10 @@ class ConfigFixture(fixture.GabbiFixture):
os.path.abspath('etc/cloudkitty/policy.json'), os.path.abspath('etc/cloudkitty/policy.json'),
group='oslo_policy', group='oslo_policy',
enforce_type=True) enforce_type=True)
conf.set_override('api_paste_config',
os.path.abspath(
'cloudkitty/tests/gabbi/gabbi_paste.ini')
)
conf.import_group('storage', 'cloudkitty.storage') conf.import_group('storage', 'cloudkitty.storage')
conf.set_override('backend', 'sqlalchemy', 'storage', conf.set_override('backend', 'sqlalchemy', 'storage',
enforce_type=True) enforce_type=True)
@ -323,6 +327,27 @@ class NowStorageDataFixture(BaseStorageDataFixture):
'3d9a1b33-482f-42fd-aef9-b575a3da9369') '3d9a1b33-482f-42fd-aef9-b575a3da9369')
class CORSConfigFixture(fixture.GabbiFixture):
"""Inject mock configuration for the CORS middleware."""
def start_fixture(self):
# Here we monkeypatch GroupAttr.__getattr__, necessary because the
# paste.ini method of initializing this middleware creates its own
# ConfigOpts instance, bypassing the regular config fixture.
def _mock_getattr(instance, key):
if key != 'allowed_origin':
return self._original_call_method(instance, key)
return "http://valid.example.com"
self._original_call_method = cfg.ConfigOpts.GroupAttr.__getattr__
cfg.ConfigOpts.GroupAttr.__getattr__ = _mock_getattr
def stop_fixture(self):
"""Remove the monkeypatch."""
cfg.ConfigOpts.GroupAttr.__getattr__ = self._original_call_method
def setup_app(): def setup_app():
rpc.init() rpc.init()
return app.setup_app() return app.load_app()

View File

@ -0,0 +1,15 @@
# This is a custom paste.ini for our gabbi tests. It enables all middleware
# except for auth tokens.
[pipeline:main]
pipeline = cors request_id ck_api_v1
[app:ck_api_v1]
paste.app_factory = cloudkitty.api.app:app_factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = cloudkitty

View File

@ -0,0 +1,40 @@
fixtures:
- ConfigFixture
- CORSConfigFixture
tests:
- name: valid cors options
OPTIONS: /
status: 200
request_headers:
origin: http://valid.example.com
access-control-request-method: GET
response_headers:
access-control-allow-origin: http://valid.example.com
- name: invalid cors options
OPTIONS: /
status: 200
request_headers:
origin: http://invalid.example.com
access-control-request-method: GET
response_forbidden_headers:
- access-control-allow-origin
- name: valid cors get
GET: /
status: 200
request_headers:
origin: http://valid.example.com
access-control-request-method: GET
response_headers:
access-control-allow-origin: http://valid.example.com
- name: invalid cors get
GET: /
status: 200
request_headers:
origin: http://invalid.example.com
response_forbidden_headers:
- access-control-allow-origin

View File

@ -1,5 +1,5 @@
[pipeline:main] [pipeline:main]
pipeline = request_id authtoken ck_api_v1 pipeline = cors request_id authtoken ck_api_v1
[app:ck_api_v1] [app:ck_api_v1]
paste.app_factory = cloudkitty.api.app:app_factory paste.app_factory = cloudkitty.api.app:app_factory
@ -10,3 +10,7 @@ paste.filter_factory = cloudkitty.api.middleware:AuthTokenMiddleware.factory
[filter:request_id] [filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = cloudkitty

View File

@ -126,6 +126,66 @@
#services = compute,image,volume,network.bw.in,network.bw.out,network.floating #services = compute,image,volume,network.bw.in,network.bw.out,network.floating
[cors]
#
# From oslo.middleware.cors
#
# Indicate whether this resource may be shared with the domain
# received in the requests "origin" header. (list value)
#allowed_origin = <None>
# Indicate that the actual request can include user credentials
# (boolean value)
#allow_credentials = true
# Indicate which headers are safe to expose to the API. Defaults to
# HTTP Simple Headers. (list value)
#expose_headers = X-Auth-Token,X-Subject-Token,X-Service-Token,X-OpenStack-Request-ID,X-Server-Management-Url
# Maximum cache age of CORS preflight requests. (integer value)
#max_age = 3600
# Indicate which methods can be used during the actual request. (list
# value)
#allow_methods = GET,PUT,POST,DELETE,PATCH
# Indicate which header field names may be used during the actual
# request. (list value)
#allow_headers = X-Auth-Token,X-Identity-Status,X-Roles,X-Service-Catalog,X-User-Id,X-Tenant-Id,X-OpenStack-Request-ID,X-Server-Management-Url
[cors.subdomain]
#
# From oslo.middleware.cors
#
# Indicate whether this resource may be shared with the domain
# received in the requests "origin" header. (list value)
#allowed_origin = <None>
# Indicate that the actual request can include user credentials
# (boolean value)
#allow_credentials = true
# Indicate which headers are safe to expose to the API. Defaults to
# HTTP Simple Headers. (list value)
#expose_headers = X-Auth-Token,X-Subject-Token,X-Service-Token,X-OpenStack-Request-ID,X-Server-Management-Url
# Maximum cache age of CORS preflight requests. (integer value)
#max_age = 3600
# Indicate which methods can be used during the actual request. (list
# value)
#allow_methods = GET,PUT,POST,DELETE,PATCH
# Indicate which header field names may be used during the actual
# request. (list value)
#allow_headers = X-Auth-Token,X-Identity-Status,X-Roles,X-Service-Catalog,X-User-Id,X-Tenant-Id,X-OpenStack-Request-ID,X-Server-Management-Url
[database] [database]
# #

View File

@ -5,5 +5,6 @@ namespace = oslo.concurrency
namespace = oslo.db namespace = oslo.db
namespace = oslo.log namespace = oslo.log
namespace = oslo.messaging namespace = oslo.messaging
namespace = oslo.middleware.cors
namespace = oslo.policy namespace = oslo.policy
namespace = keystonemiddleware.auth_token namespace = keystonemiddleware.auth_token

View File

@ -33,6 +33,9 @@ console_scripts =
oslo.config.opts = oslo.config.opts =
cloudkitty.common.config = cloudkitty.common.config:list_opts cloudkitty.common.config = cloudkitty.common.config:list_opts
oslo.config.opts.defaults =
cloudkitty.common.config = cloudkitty.common.defaults:set_cors_middleware_defaults
cloudkitty.collector.backends = cloudkitty.collector.backends =
fake = cloudkitty.collector.fake:CSVCollector fake = cloudkitty.collector.fake:CSVCollector
ceilometer = cloudkitty.collector.ceilometer:CeilometerCollector ceilometer = cloudkitty.collector.ceilometer:CeilometerCollector