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
--- 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