From d9509e294fb700b6130e0a2405a6986737da7a7d Mon Sep 17 00:00:00 2001
From: Chris Dent <cdent@anticdent.org>
Date: Wed, 2 Nov 2016 17:01:36 +0000
Subject: [PATCH] Add rudimentary CORS support to placement API

If 'cors.allowed_origin' is set in the nova.conf, configure the
placement API to use oslo_middleware.CORS. Simple gabbi tests are
added which confirm the basic operation of the middleware, modeled
on the tests in nova/tests/functional/test_middleware.py, as well as
additional tests which confirm that when the middleware is not
configured it is not present in the system.

The cors config options are registered in deploy.py to ensure that
the group is allowed to exist in conf (even if it doesn't). Without
that, a deployment that tries to configure cors would not actually
cause the middleware to run.

Change-Id: I571bc675facaecb523dcf906f4bb44a51102b514
---
 nova/api/openstack/placement/deploy.py        | 19 ++++++--
 .../api/openstack/placement/fixtures.py       | 24 +++++++++-
 .../placement/gabbits/basic-http.yaml         | 10 ++++
 .../api/openstack/placement/gabbits/cors.yaml | 47 +++++++++++++++++++
 .../openstack/placement/gabbits/non-cors.yaml | 25 ++++++++++
 .../placement-cors-c7a83e8c63787736.yaml      |  9 ++++
 6 files changed, 130 insertions(+), 4 deletions(-)
 create mode 100644 nova/tests/functional/api/openstack/placement/gabbits/cors.yaml
 create mode 100644 nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml
 create mode 100644 releasenotes/notes/placement-cors-c7a83e8c63787736.yaml

diff --git a/nova/api/openstack/placement/deploy.py b/nova/api/openstack/placement/deploy.py
index 1edf9724b..34001f34a 100644
--- a/nova/api/openstack/placement/deploy.py
+++ b/nova/api/openstack/placement/deploy.py
@@ -12,7 +12,8 @@
 """Deployment handling for Placmenent API."""
 
 from keystonemiddleware import auth_token
-from oslo_middleware import request_id
+import oslo_middleware
+from oslo_middleware import cors
 
 from nova.api import openstack as common_api
 from nova.api.openstack.placement import auth
@@ -41,8 +42,18 @@ def deploy(conf, project_name):
         auth_middleware = auth_token.filter_factory(
             {}, oslo_config_project=project_name)
 
+    # Pass in our CORS config, if any, manually as that's a)
+    # explicit, b) makes testing more straightfoward, c) let's
+    # us control the use of cors by the presence of its config.
+    conf.register_opts(cors.CORS_OPTS, 'cors')
+    if conf.cors.allowed_origin:
+        cors_middleware = oslo_middleware.CORS.factory(
+            {}, **conf.cors)
+    else:
+        cors_middleware = None
+
     context_middleware = auth.PlacementKeystoneContext
-    req_id_middleware = request_id.RequestId
+    req_id_middleware = oslo_middleware.RequestId
     microversion_middleware = microversion.MicroversionMiddleware
     fault_wrap = common_api.FaultWrapper
     request_log = requestlog.RequestLog
@@ -62,9 +73,11 @@ def deploy(conf, project_name):
                        request_log,
                        context_middleware,
                        auth_middleware,
+                       cors_middleware,
                        req_id_middleware,
                        ):
-        application = middleware(application)
+        if middleware:
+            application = middleware(application)
 
     return application
 
diff --git a/nova/tests/functional/api/openstack/placement/fixtures.py b/nova/tests/functional/api/openstack/placement/fixtures.py
index 8a3d6106b..f766728ae 100644
--- a/nova/tests/functional/api/openstack/placement/fixtures.py
+++ b/nova/tests/functional/api/openstack/placement/fixtures.py
@@ -13,6 +13,7 @@
 import os
 
 from gabbi import fixture
+from oslo_middleware import cors
 from oslo_utils import uuidutils
 
 from nova.api.openstack.placement import deploy
@@ -55,7 +56,16 @@ class APIFixture(fixture.GabbiFixture):
                                group='api_database')
         self.conf.set_override('connection', "sqlite://",
                                group='placement_database')
-        config.parse_args([], default_config_files=None, configure_db=False,
+
+        # Register CORS opts, but do not set config. This has the
+        # effect of exercising the "don't use cors" path in
+        # deploy.py. Without setting some config the group will not
+        # be present.
+        self.conf.register_opts(cors.CORS_OPTS, 'cors')
+
+        # Make sure default_config_files is an empty list, not None.
+        # If None /etc/nova/nova.conf is read and confuses results.
+        config.parse_args([], default_config_files=[], configure_db=False,
                           init_rpc=False)
 
         # NOTE(cdent): api and main database are not used but we still need
@@ -136,3 +146,15 @@ class AllocationFixture(APIFixture):
         # not been created in the Allocation fixture
         os.environ['ALT_RP_UUID'] = uuidutils.generate_uuid()
         os.environ['ALT_RP_NAME'] = uuidutils.generate_uuid()
+
+
+class CORSFixture(APIFixture):
+    """An APIFixture that turns on CORS."""
+
+    def start_fixture(self):
+        super(CORSFixture, self).start_fixture()
+        # NOTE(cdent): If we remove this override, then the cors
+        # group ends up not existing in the conf, so when deploy.py
+        # wants to load the CORS middleware, it will not.
+        self.conf.set_override('allowed_origin', 'http://valid.example.com',
+                               group='cors')
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml b/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml
index 00757ebba..fae0d2c24 100644
--- a/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml
+++ b/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml
@@ -52,6 +52,16 @@ tests:
   response_strings:
     - The method DELETE is not allowed for this resource.
 
+- name: 405 on bad options method on app
+  OPTIONS: /resource_providers
+  status: 405
+  response_headers:
+      allow: /(GET|POST), (POST|GET)/
+  response_json_paths:
+      $.errors[0].title: Method Not Allowed
+  response_strings:
+    - The method OPTIONS is not allowed for this resource.
+
 - name: bad accept resource providers
   GET: /resource_providers
   request_headers:
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/cors.yaml b/nova/tests/functional/api/openstack/placement/gabbits/cors.yaml
new file mode 100644
index 000000000..291e1d5f7
--- /dev/null
+++ b/nova/tests/functional/api/openstack/placement/gabbits/cors.yaml
@@ -0,0 +1,47 @@
+# Confirm that CORS is present. No complex configuration is done so
+# this just tests the basics. Borrowed, in spirit, from
+# nova.tests.functional.test_middleware.
+
+fixtures:
+    - CORSFixture
+
+defaults:
+    request_headers:
+        x-auth-token: user
+
+tests:
+- name: valid options request
+  OPTIONS: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_headers:
+      access-control-allow-origin: http://valid.example.com
+
+- name: invalid options request
+  OPTIONS: /
+  request_headers:
+      origin: http://invalid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_forbidden_headers:
+      - access-control-allow-origin
+
+- name: valid get request
+  GET: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_headers:
+      access-control-allow-origin: http://valid.example.com
+
+- name: invalid get request
+  GET: /
+  request_headers:
+      origin: http://invalid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_forbidden_headers:
+      - access-control-allow-origin
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml b/nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml
new file mode 100644
index 000000000..b0b974cc5
--- /dev/null
+++ b/nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml
@@ -0,0 +1,25 @@
+# Confirm that things work as intended when CORS is not configured.
+
+fixtures:
+    - APIFixture
+
+defaults:
+    request_headers:
+        x-auth-token: user
+
+tests:
+- name: options request not allowed
+  OPTIONS: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 405
+
+- name: get request no cors headers
+  GET: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_forbidden_headers:
+      - access-control-allow-origin
diff --git a/releasenotes/notes/placement-cors-c7a83e8c63787736.yaml b/releasenotes/notes/placement-cors-c7a83e8c63787736.yaml
new file mode 100644
index 000000000..6ea9af445
--- /dev/null
+++ b/releasenotes/notes/placement-cors-c7a83e8c63787736.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    The placement API service can now be configured to support
+    `CORS <http://docs.openstack.org/developer/oslo.middleware/cors.html>`_.
+    If a `cors` configuration group is present in the service's configuration
+    file (currently `nova.conf`), with `allowed_origin` configured, the values
+    within will be used to configure the middleware. If `cors.allowed_origin`
+    is not set, the middleware will not be used.