Added CORS support to Ironic Inspector

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

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: I467d4e14b27f1d4808786d431aff66808c707a99
This commit is contained in:
Jiri Tomasek 2016-03-07 21:52:23 +01:00
parent aea60cdc4a
commit 19fe16fd42
9 changed files with 123 additions and 10 deletions

View File

@ -7,3 +7,4 @@ namespace = ironic_inspector.plugins.discovery
namespace = keystonemiddleware.auth_token namespace = keystonemiddleware.auth_token
namespace = oslo.db namespace = oslo.db
namespace = oslo.log namespace = oslo.log
namespace = oslo.middleware.cors

View File

@ -166,6 +166,66 @@
#fatal_deprecations = false #fatal_deprecations = false
[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 = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma
# 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,POST,PUT,HEAD,PATCH,DELETE,OPTIONS
# Indicate which header field names may be used during the actual
# request. (list value)
#allow_headers = X-Auth-Token,X-OpenStack-Ironic-Inspector-API-Minimum-Version,X-OpenStack-Ironic-Inspector-API-Maximum-Version,X-OpenStack-Ironic-Inspector-API-Version
[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 = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma
# 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,POST,PUT,HEAD,PATCH,DELETE,OPTIONS
# Indicate which header field names may be used during the actual
# request. (list value)
#allow_headers = X-Auth-Token,X-OpenStack-Ironic-Inspector-API-Minimum-Version,X-OpenStack-Ironic-Inspector-API-Maximum-Version,X-OpenStack-Ironic-Inspector-API-Version
[database] [database]
# #

View File

@ -12,8 +12,13 @@
# limitations under the License. # limitations under the License.
from oslo_config import cfg from oslo_config import cfg
from oslo_middleware import cors
MIN_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Minimum-Version'
MAX_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Maximum-Version'
VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Version'
VALID_ADD_PORTS_VALUES = ('all', 'active', 'pxe') VALID_ADD_PORTS_VALUES = ('all', 'active', 'pxe')
VALID_KEEP_PORTS_VALUES = ('all', 'present', 'added') VALID_KEEP_PORTS_VALUES = ('all', 'present', 'added')
VALID_STORE_DATA_VALUES = ('none', 'swift') VALID_STORE_DATA_VALUES = ('none', 'swift')
@ -215,3 +220,22 @@ def list_opts():
('processing', PROCESSING_OPTS), ('processing', PROCESSING_OPTS),
('discoverd', DISCOVERD_OPTS), ('discoverd', DISCOVERD_OPTS),
] ]
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."""
# TODO(krotscheck): Update with https://review.openstack.org/#/c/285368/
cfg.set_defaults(
cors.CORS_OPTS,
allow_headers=['X-Auth-Token',
MIN_VERSION_HEADER,
MAX_VERSION_HEADER,
VERSION_HEADER],
allow_methods=['GET', 'POST', 'PUT', 'HEAD',
'PATCH', 'DELETE', 'OPTIONS']
)

View File

@ -48,9 +48,6 @@ LOG = utils.getProcessingLogger(__name__)
MINIMUM_API_VERSION = (1, 0) MINIMUM_API_VERSION = (1, 0)
CURRENT_API_VERSION = (1, 3) CURRENT_API_VERSION = (1, 3)
_MIN_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Minimum-Version'
_MAX_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Maximum-Version'
_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Version'
_LOGGING_EXCLUDED_KEYS = ('logs',) _LOGGING_EXCLUDED_KEYS = ('logs',)
@ -89,7 +86,7 @@ def convert_exceptions(func):
@app.before_request @app.before_request
def check_api_version(): def check_api_version():
requested = flask.request.headers.get(_VERSION_HEADER, requested = flask.request.headers.get(conf.VERSION_HEADER,
_DEFAULT_API_VERSION) _DEFAULT_API_VERSION)
try: try:
requested = tuple(int(x) for x in requested.split('.')) requested = tuple(int(x) for x in requested.split('.'))
@ -108,8 +105,8 @@ def check_api_version():
@app.after_request @app.after_request
def add_version_headers(res): def add_version_headers(res):
res.headers[_MIN_VERSION_HEADER] = '%s.%s' % MINIMUM_API_VERSION res.headers[conf.MIN_VERSION_HEADER] = '%s.%s' % MINIMUM_API_VERSION
res.headers[_MAX_VERSION_HEADER] = '%s.%s' % CURRENT_API_VERSION res.headers[conf.MAX_VERSION_HEADER] = '%s.%s' % CURRENT_API_VERSION
return res return res
@ -383,6 +380,8 @@ class Service(object):
LOG.info(_LI('Introspection data will be stored in Swift in the ' LOG.info(_LI('Introspection data will be stored in Swift in the '
'container %s'), CONF.swift.container) 'container %s'), CONF.swift.container)
utils.add_cors_middleware(app)
db.init() db.init()
try: try:

View File

@ -20,6 +20,7 @@ import mock
from oslo_utils import uuidutils from oslo_utils import uuidutils
from ironic_inspector.common import ironic as ir_utils from ironic_inspector.common import ironic as ir_utils
from ironic_inspector import conf
from ironic_inspector import db from ironic_inspector import db
from ironic_inspector import firewall from ironic_inspector import firewall
from ironic_inspector import introspect from ironic_inspector import introspect
@ -352,9 +353,9 @@ class TestApiMisc(BaseAPITest):
class TestApiVersions(BaseAPITest): class TestApiVersions(BaseAPITest):
def _check_version_present(self, res): def _check_version_present(self, res):
self.assertEqual('%d.%d' % main.MINIMUM_API_VERSION, self.assertEqual('%d.%d' % main.MINIMUM_API_VERSION,
res.headers.get(main._MIN_VERSION_HEADER)) res.headers.get(conf.MIN_VERSION_HEADER))
self.assertEqual('%d.%d' % main.CURRENT_API_VERSION, self.assertEqual('%d.%d' % main.CURRENT_API_VERSION,
res.headers.get(main._MAX_VERSION_HEADER)) res.headers.get(conf.MAX_VERSION_HEADER))
def test_root_endpoint(self): def test_root_endpoint(self):
res = self.app.get("/") res = self.app.get("/")
@ -420,14 +421,14 @@ class TestApiVersions(BaseAPITest):
self.app.post('/v1/introspection/foobar')) self.app.post('/v1/introspection/foobar'))
def test_request_correct_version(self): def test_request_correct_version(self):
headers = {main._VERSION_HEADER: headers = {conf.VERSION_HEADER:
main._format_version(main.CURRENT_API_VERSION)} main._format_version(main.CURRENT_API_VERSION)}
self._check_version_present(self.app.get('/', headers=headers)) self._check_version_present(self.app.get('/', headers=headers))
def test_request_unsupported_version(self): def test_request_unsupported_version(self):
bad_version = (main.CURRENT_API_VERSION[0], bad_version = (main.CURRENT_API_VERSION[0],
main.CURRENT_API_VERSION[1] + 1) main.CURRENT_API_VERSION[1] + 1)
headers = {main._VERSION_HEADER: headers = {conf.VERSION_HEADER:
main._format_version(bad_version)} main._format_version(bad_version)}
res = self.app.get('/', headers=headers) res = self.app.get('/', headers=headers)
self._check_version_present(res) self._check_version_present(res)

View File

@ -18,6 +18,7 @@ import futurist
from keystonemiddleware import auth_token from keystonemiddleware import auth_token
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from oslo_middleware import cors as cors_middleware
import six import six
from ironic_inspector.common.i18n import _, _LE from ironic_inspector.common.i18n import _, _LE
@ -159,6 +160,17 @@ def add_auth_middleware(app):
app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, auth_conf) app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, auth_conf)
def add_cors_middleware(app):
"""Create a CORS wrapper
Attach ironic-inspector-specific defaults that must be included
in all CORS responses.
:param app: application
"""
app.wsgi_app = cors_middleware.CORS(app.wsgi_app, CONF)
def check_auth(request): def check_auth(request):
"""Check authentication on request. """Check authentication on request.

View File

@ -0,0 +1,13 @@
---
features:
- |
Added CORS support middleware to Ironic Inspector, allowing a deployer
to optionally configure rules under which a javascript client may
break the single-origin policy and access the API directly.
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

View File

@ -19,6 +19,7 @@ oslo.config>=3.7.0 # Apache-2.0
oslo.db>=4.1.0 # Apache-2.0 oslo.db>=4.1.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0 oslo.log>=1.14.0 # Apache-2.0
oslo.middleware>=3.0.0 # Apache-2.0
oslo.rootwrap>=2.0.0 # Apache-2.0 oslo.rootwrap>=2.0.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0
six>=1.9.0 # MIT six>=1.9.0 # MIT

View File

@ -58,6 +58,8 @@ oslo.config.opts =
ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts
ironic_inspector.common.swift = ironic_inspector.common.swift:list_opts ironic_inspector.common.swift = ironic_inspector.common.swift:list_opts
ironic_inspector.plugins.discovery = ironic_inspector.plugins.discovery:list_opts ironic_inspector.plugins.discovery = ironic_inspector.plugins.discovery:list_opts
oslo.config.opts.defaults =
ironic_inspector = ironic_inspector.conf:set_config_defaults
[compile_catalog] [compile_catalog]
directory = ironic_inspector/locale directory = ironic_inspector/locale