autoland-webapi: add cors headers to public api paths from config option (bug 1345163). r?smacleod draft
authorIsrael Madueme <imadueme@mozilla.com>
Mon, 27 Feb 2017 17:55:30 -0500
changeset 325 3f19d9a6b80cefa4fa815ea61f95a9eefc2129c1
parent 298 aa672fb8a9c0faa7af7131954643858da83bba2b
push id149
push userbmo:imadueme@mozilla.com
push dateTue, 07 Mar 2017 22:16:14 +0000
reviewerssmacleod
bugs1345163
autoland-webapi: add cors headers to public api paths from config option (bug 1345163). r?smacleod Added a base RequestHandler for the public api routes that will set the 'Access-Control-Allow-Origin' to allow requests from the UI origin, where ever it may be. This is controlled by the 'cors_allowed_origins' option when the app is created. If cors_allowed_origins is unset or empty then no CORS header will be set, thus not allowing any requests from any other origin. MozReview-Commit-ID: E40x6kuXZAb
autoland/docker-compose.yml
autoland/docker/web/nginx-conf.d/default.conf
autoland/webapi/autolandweb/routes.py
autoland/webapi/autolandweb/server.py
autoland/webapi/tests/test_cors.py
--- a/autoland/docker-compose.yml
+++ b/autoland/docker-compose.yml
@@ -29,16 +29,17 @@ services:
       context: docker/mountebank/
     command: start --logfile /mb.log --debug --mock
 
   webapi:
     build:
       context: ./webapi
       dockerfile: ./Dockerfile-dev
     volumes:
-     - ./webapi:/app
+      - ./webapi:/app
     environment:
-     - AUTOLANDWEB_DEBUG=1
-     - AUTOLANDWEB_PORT=9090
-     - AUTOLANDWEB_PRETTY_LOG=1
-     - AUTOLANDWEB_VERSION_PATH=/version.json
+      - AUTOLANDWEB_DEBUG=1
+      - AUTOLANDWEB_PORT=9090
+      - AUTOLANDWEB_PRETTY_LOG=1
+      - AUTOLANDWEB_VERSION_PATH=/version.json
+      - AUTOLANDWEB_CORS_ALLOWED_ORIGINS=*
     depends_on:
-      - mountebank
\ No newline at end of file
+      - mountebank
--- a/autoland/docker/web/nginx-conf.d/default.conf
+++ b/autoland/docker/web/nginx-conf.d/default.conf
@@ -9,13 +9,11 @@ server {
     }
 }
 
 server {
     listen       9999;
     server_name  localhost;
 
     location ~ /.* {
-        # Make sure to update the CORS allowed origins for production.
-        add_header Access-Control-Allow-Origin '*' always;
         proxy_pass http://webapi:9090;
     }
 }
--- a/autoland/webapi/autolandweb/routes.py
+++ b/autoland/webapi/autolandweb/routes.py
@@ -4,22 +4,31 @@
 
 import tornado.httpclient
 import tornado.web
 
 from autolandweb.dockerflow import DOCKERFLOW_ROUTES
 from autolandweb.series import get_series_status
 
 
-class MainHandler(tornado.web.RequestHandler):
+class PublicApiHandler(tornado.web.RequestHandler):
+    def set_default_headers(self):
+        if self.settings['cors_allowed_origins']:
+            self.set_header(
+                'Access-Control-Allow-Origin',
+                self.settings['cors_allowed_origins']
+            )
+
+
+class MainHandler(PublicApiHandler):
     def get(self):
         self.write('Hello, from Autoland')
 
 
-class ReposHandler(tornado.web.RequestHandler):
+class ReposHandler(PublicApiHandler):
     """Handler for repositories."""
 
     async def get(self, repo=None):
         if repo is None:
             return
 
         http = tornado.httpclient.AsyncHTTPClient()
         # FIXME: We don't validate the user input here!  Security problem?
@@ -39,17 +48,17 @@ class ReposHandler(tornado.web.RequestHa
         elif response.code == 404:
             self.set_status(404)
             self.write({'error': 'Repo not found'})
         else:
             # Everything not 2XX or 404 is an exception.
             response.rethrow()
 
 
-class SeriesHandler(tornado.web.RequestHandler):
+class SeriesHandler(PublicApiHandler):
     """Handler for series'."""
 
     async def get(self, repo, series=None):
         if series is None:
             self.write({})
             return
 
         status = await get_series_status(repo, series)
--- a/autoland/webapi/autolandweb/server.py
+++ b/autoland/webapi/autolandweb/server.py
@@ -12,17 +12,22 @@ import tornado.web
 
 from autolandweb.dockerflow import read_version
 from autolandweb.mozlog import get_mozlog_config, tornado_log_function
 from autolandweb.routes import ROUTES
 
 logger = logging.getLogger(__name__)
 
 
-def make_app(debug=False, version_data=None, reviewboard_url=''):
+def make_app(
+    debug=False,
+    version_data=None,
+    reviewboard_url='',
+    cors_allowed_origins=None
+):
     """Construct a fully configured Tornado Application object.
 
     Leaving out the version_data argument may lead to unexpected behaviour.
 
     Args:
         debug: Optional boolean, turns on the Tornado application server's
             debug mode.
         version_data: A dictionary with keys and data matching the Dockerflow
@@ -32,43 +37,52 @@ def make_app(debug=False, version_data=N
         reviewboard_url: Optional string, the URL of the reviewboard host and
             port to use for API requests. (e.g. 'http://foo.something:0000')
     """
     return tornado.web.Application(
         ROUTES,
         debug=debug,
         log_function=tornado_log_function,
         version_data=version_data,
-        reviewboard_url=reviewboard_url
+        reviewboard_url=reviewboard_url,
+        cors_allowed_origins=cors_allowed_origins
     )
 
 
 @click.command()
 @click.option('--debug', envvar='AUTOLANDWEB_DEBUG', is_flag=True)
 @click.option('--reviewboard-url', envvar='REVIEWBOARD_URL', default='')
 @click.option('--port', envvar='AUTOLANDWEB_PORT', default=8888)
 @click.option('--pretty-log', envvar='AUTOLANDWEB_PRETTY_LOG', default=False)
 @click.option(
     '--version-path',
     envvar='AUTOLANDWEB_VERSION_PATH',
     default='/app/version.json'
 )
-def autolandweb(debug, reviewboard_url, port, pretty_log, version_path):
+@click.option(
+    '--cors-allowed-origins',
+    envvar='AUTOLANDWEB_CORS_ALLOWED_ORIGINS',
+    default=None
+)
+def autolandweb(
+    debug, reviewboard_url, port, pretty_log, version_path,
+    cors_allowed_origins
+):
     logging_config = get_mozlog_config(debug=debug, pretty=pretty_log)
     logging.config.dictConfig(logging_config)
 
     version_data = read_version(version_path)
     if not version_data:
         logger.critical(
             {
                 'msg': 'Could not load version.json, shutting down',
                 'path': version_path,
             }, 'app.fatal'
         )
         sys.exit(1)
 
-    app = make_app(debug, version_data, reviewboard_url)
+    app = make_app(debug, version_data, reviewboard_url, cors_allowed_origins)
     app.listen(port)
     tornado.ioloop.IOLoop.current().start()
 
 
 if __name__ == '__main__':
     autolandweb()
new file mode 100644
--- /dev/null
+++ b/autoland/webapi/tests/test_cors.py
@@ -0,0 +1,38 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+Tests to ensure that cors headers are properly set.
+"""
+
+import pytest
+
+from autolandweb.server import make_app
+
+
+@pytest.fixture
+def app():
+    """Returns the tornado.Application instance we'll be testing against.
+
+    Required for pytest-tornado to function.
+    """
+    return make_app()
+
+
+@pytest.mark.gen_test
+async def test_cors_unset_without_config_option(http_client, base_url, app):
+    root_url = base_url + '/'
+    app.settings['cors_allowed_origins'] = None
+    response = await http_client.fetch(root_url)
+    assert response.code == 200
+    assert not response.headers.get_list('Access-Control-Allow-Origin')
+
+
+@pytest.mark.gen_test
+async def test_cors_header_set_from_config(http_client, base_url, app):
+    root_url = base_url + '/'
+    allowed_origins = 'https://autoland.mozilla.org'
+    app.settings['cors_allowed_origins'] = allowed_origins
+    response = await http_client.fetch(root_url)
+    assert response.code == 200
+    assert response.headers['Access-Control-Allow-Origin'] == allowed_origins