From 19fe16fd4265e33089d7fe9487b076917ece14d1 Mon Sep 17 00:00:00 2001
From: Jiri Tomasek <jtomasek@redhat.com>
Date: Mon, 7 Mar 2016 21:52:23 +0100
Subject: [PATCH] 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
---
 config-generator.conf                         |  1 +
 example.conf                                  | 60 +++++++++++++++++++
 ironic_inspector/conf.py                      | 24 ++++++++
 ironic_inspector/main.py                      | 11 ++--
 ironic_inspector/test/test_main.py            |  9 +--
 ironic_inspector/utils.py                     | 12 ++++
 releasenotes/notes/cors-5f345c65da7f5c99.yaml | 13 ++++
 requirements.txt                              |  1 +
 setup.cfg                                     |  2 +
 9 files changed, 123 insertions(+), 10 deletions(-)
 create mode 100644 releasenotes/notes/cors-5f345c65da7f5c99.yaml

diff --git a/config-generator.conf b/config-generator.conf
index ac71fb450..054f5f6ae 100644
--- a/config-generator.conf
+++ b/config-generator.conf
@@ -7,3 +7,4 @@ namespace = ironic_inspector.plugins.discovery
 namespace = keystonemiddleware.auth_token
 namespace = oslo.db
 namespace = oslo.log
+namespace = oslo.middleware.cors
diff --git a/example.conf b/example.conf
index 57fac5e94..c1291b93b 100644
--- a/example.conf
+++ b/example.conf
@@ -166,6 +166,66 @@
 #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]
 
 #
diff --git a/ironic_inspector/conf.py b/ironic_inspector/conf.py
index be2b6f5d4..3860e5f20 100644
--- a/ironic_inspector/conf.py
+++ b/ironic_inspector/conf.py
@@ -12,8 +12,13 @@
 # limitations under the License.
 
 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_KEEP_PORTS_VALUES = ('all', 'present', 'added')
 VALID_STORE_DATA_VALUES = ('none', 'swift')
@@ -215,3 +220,22 @@ def list_opts():
         ('processing', PROCESSING_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']
+    )
diff --git a/ironic_inspector/main.py b/ironic_inspector/main.py
index b124df1a9..1ac67bbd7 100644
--- a/ironic_inspector/main.py
+++ b/ironic_inspector/main.py
@@ -48,9 +48,6 @@ LOG = utils.getProcessingLogger(__name__)
 
 MINIMUM_API_VERSION = (1, 0)
 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',)
 
 
@@ -89,7 +86,7 @@ def convert_exceptions(func):
 
 @app.before_request
 def check_api_version():
-    requested = flask.request.headers.get(_VERSION_HEADER,
+    requested = flask.request.headers.get(conf.VERSION_HEADER,
                                           _DEFAULT_API_VERSION)
     try:
         requested = tuple(int(x) for x in requested.split('.'))
@@ -108,8 +105,8 @@ def check_api_version():
 
 @app.after_request
 def add_version_headers(res):
-    res.headers[_MIN_VERSION_HEADER] = '%s.%s' % MINIMUM_API_VERSION
-    res.headers[_MAX_VERSION_HEADER] = '%s.%s' % CURRENT_API_VERSION
+    res.headers[conf.MIN_VERSION_HEADER] = '%s.%s' % MINIMUM_API_VERSION
+    res.headers[conf.MAX_VERSION_HEADER] = '%s.%s' % CURRENT_API_VERSION
     return res
 
 
@@ -383,6 +380,8 @@ class Service(object):
             LOG.info(_LI('Introspection data will be stored in Swift in the '
                          'container %s'), CONF.swift.container)
 
+        utils.add_cors_middleware(app)
+
         db.init()
 
         try:
diff --git a/ironic_inspector/test/test_main.py b/ironic_inspector/test/test_main.py
index 958bbbf9d..d400ae900 100644
--- a/ironic_inspector/test/test_main.py
+++ b/ironic_inspector/test/test_main.py
@@ -20,6 +20,7 @@ import mock
 from oslo_utils import uuidutils
 
 from ironic_inspector.common import ironic as ir_utils
+from ironic_inspector import conf
 from ironic_inspector import db
 from ironic_inspector import firewall
 from ironic_inspector import introspect
@@ -352,9 +353,9 @@ class TestApiMisc(BaseAPITest):
 class TestApiVersions(BaseAPITest):
     def _check_version_present(self, res):
         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,
-                         res.headers.get(main._MAX_VERSION_HEADER))
+                         res.headers.get(conf.MAX_VERSION_HEADER))
 
     def test_root_endpoint(self):
         res = self.app.get("/")
@@ -420,14 +421,14 @@ class TestApiVersions(BaseAPITest):
             self.app.post('/v1/introspection/foobar'))
 
     def test_request_correct_version(self):
-        headers = {main._VERSION_HEADER:
+        headers = {conf.VERSION_HEADER:
                    main._format_version(main.CURRENT_API_VERSION)}
         self._check_version_present(self.app.get('/', headers=headers))
 
     def test_request_unsupported_version(self):
         bad_version = (main.CURRENT_API_VERSION[0],
                        main.CURRENT_API_VERSION[1] + 1)
-        headers = {main._VERSION_HEADER:
+        headers = {conf.VERSION_HEADER:
                    main._format_version(bad_version)}
         res = self.app.get('/', headers=headers)
         self._check_version_present(res)
diff --git a/ironic_inspector/utils.py b/ironic_inspector/utils.py
index 679f48315..d46bef9ad 100644
--- a/ironic_inspector/utils.py
+++ b/ironic_inspector/utils.py
@@ -18,6 +18,7 @@ import futurist
 from keystonemiddleware import auth_token
 from oslo_config import cfg
 from oslo_log import log
+from oslo_middleware import cors as cors_middleware
 import six
 
 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)
 
 
+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):
     """Check authentication on request.
 
diff --git a/releasenotes/notes/cors-5f345c65da7f5c99.yaml b/releasenotes/notes/cors-5f345c65da7f5c99.yaml
new file mode 100644
index 000000000..ec66fef1f
--- /dev/null
+++ b/releasenotes/notes/cors-5f345c65da7f5c99.yaml
@@ -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
diff --git a/requirements.txt b/requirements.txt
index fb076cf0d..ae3600211 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,6 +19,7 @@ oslo.config>=3.7.0 # Apache-2.0
 oslo.db>=4.1.0 # Apache-2.0
 oslo.i18n>=2.1.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.utils>=3.5.0 # Apache-2.0
 six>=1.9.0 # MIT
diff --git a/setup.cfg b/setup.cfg
index 583b22865..72b60d0be 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -58,6 +58,8 @@ oslo.config.opts =
     ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts
     ironic_inspector.common.swift = ironic_inspector.common.swift: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]
 directory = ironic_inspector/locale