Skip to content

Commit

Permalink
CORS headers added to Flask responses.
Browse files Browse the repository at this point in the history
The method to set CORS headers moved to `views` module and CKAN core
refactored to import and use it.
  • Loading branch information
brew committed Jun 14, 2016
1 parent 7995561 commit 77a436e
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 30 deletions.
7 changes: 6 additions & 1 deletion ckan/config/middleware/flask_app.py
Expand Up @@ -26,7 +26,7 @@
from ckan.common import c
from ckan.plugins import PluginImplementations
from ckan.plugins.interfaces import IBlueprint
from ckan.views import identify_user
from ckan.views import identify_user, set_cors_headers_for_response

from ckan.config.middleware import common_middleware

Expand Down Expand Up @@ -98,6 +98,11 @@ def save_session(self, app, session, response):
def ckan_before_request():
identify_user()

@app.after_request
def ckan_after_request(response):
set_cors_headers_for_response(response)
return response

# Template context processors
@app.context_processor
def helper_functions():
Expand Down
27 changes: 2 additions & 25 deletions ckan/lib/base.py
Expand Up @@ -7,7 +7,6 @@
import logging
import time

from paste.deploy.converters import asbool
from pylons import cache, config, session
from pylons.controllers import WSGIController
from pylons.controllers.util import abort as _abort
Expand All @@ -26,6 +25,7 @@
import ckan.plugins as p
import ckan.model as model
import ckan.lib.maintain as maintain
from ckan.views import identify_user, set_cors_headers_for_response

# These imports are for legacy usages and will be removed soon these should
# be imported directly from ckan.common for internal ckan code and via the
Expand Down Expand Up @@ -218,7 +218,6 @@ def _identify_user(self):
c.user = None
c.userobj = None
c.author = user's IP address (unicode)'''
from ckan.views import identify_user
identify_user()

def __call__(self, environ, start_response):
Expand Down Expand Up @@ -263,33 +262,11 @@ def __after__(self, action, **params):
# Do we have CORS settings in config?
if config.get('ckan.cors.origin_allow_all') \
and request.headers.get('Origin'):
self._set_cors()
set_cors_headers_for_response(response)
r_time = time.time() - c.__timer
url = request.environ['CKAN_CURRENT_URL'].split('?')[0]
log.info(' %s render time %.3f seconds' % (url, r_time))

def _set_cors(self):
'''
Set up Access Control Allow headers if either origin_allow_all is
True, or the request Origin is in the origin_whitelist.
'''
cors_origin_allowed = None
if asbool(config.get('ckan.cors.origin_allow_all')):
cors_origin_allowed = "*"
elif config.get('ckan.cors.origin_whitelist') and \
request.headers.get('Origin') \
in config['ckan.cors.origin_whitelist'].split(" "):
# set var to the origin to allow it.
cors_origin_allowed = request.headers.get('Origin')

if cors_origin_allowed is not None:
response.headers['Access-Control-Allow-Origin'] = \
cors_origin_allowed
response.headers['Access-Control-Allow-Methods'] = \
"POST, PUT, GET, DELETE, OPTIONS"
response.headers['Access-Control-Allow-Headers'] = \
"X-CKAN-API-KEY, Authorization, Content-Type"

def _get_page_number(self, params, key='page', default=1):
"""
Returns the page number from the provided params after
Expand Down
151 changes: 151 additions & 0 deletions ckan/tests/lib/test_base.py
Expand Up @@ -3,6 +3,7 @@
from nose import tools as nose_tools

import ckan.tests.helpers as helpers
import ckan.plugins as p


class TestRenderSnippet(helpers.FunctionalTestBase):
Expand Down Expand Up @@ -160,3 +161,153 @@ def test_cors_config_origin_allow_all_false_with_whitelist_not_containing_origin
assert 'Access-Control-Allow-Origin' not in response_headers
assert 'Access-Control-Allow-Methods' not in response_headers
assert 'Access-Control-Allow-Headers' not in response_headers


class TestCORSFlask(helpers.FunctionalTestBase):

@classmethod
def setup_class(cls):
super(TestCORSFlask, cls).setup_class()
cls.app = cls._get_test_app()
flask_app = helpers.find_flask_app(cls.app)

if not p.plugin_loaded('test_simple_flask_plugin'):
p.load('test_simple_flask_plugin')
plugin = p.get_plugin('test_simple_flask_plugin')
flask_app.register_blueprint(plugin.get_blueprint(),
prioritise_rules=True)

@classmethod
def teardown_class(cls):
super(TestCORSFlask, cls).teardown_class()
p.unload('test_simple_flask_plugin')

def test_options(self):
response = self.app.options(url='/simple_flask', status=200)
assert len(str(response.body)) == 0, 'OPTIONS must return no content'

def test_cors_config_no_cors(self):
'''
No ckan.cors settings in config, so no Access-Control-Allow headers in
response.
'''
response = self.app.get('/simple_flask')
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' not in response_headers
assert 'Access-Control-Allow-Methods' not in response_headers
assert 'Access-Control-Allow-Headers' not in response_headers

def test_cors_config_no_cors_with_origin(self):
'''
No ckan.cors settings in config, so no Access-Control-Allow headers in
response, even with origin header in request.
'''
request_headers = {'Origin': 'http://thirdpartyrequests.org'}
response = self.app.get('/simple_flask', headers=request_headers)
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' not in response_headers
assert 'Access-Control-Allow-Methods' not in response_headers
assert 'Access-Control-Allow-Headers' not in response_headers

@helpers.change_config('ckan.cors.origin_allow_all', 'true')
def test_cors_config_origin_allow_all_true_no_origin(self):
'''
With origin_allow_all set to true, but no origin in the request
header, no Access-Control-Allow headers should be in the response.
'''
response = self.app.get('/simple_flask')
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' not in response_headers
assert 'Access-Control-Allow-Methods' not in response_headers
assert 'Access-Control-Allow-Headers' not in response_headers

@helpers.change_config('ckan.cors.origin_allow_all', 'true')
@helpers.change_config('ckan.site_url', 'http://test.ckan.org')
def test_cors_config_origin_allow_all_true_with_origin(self):
'''
With origin_allow_all set to true, and an origin in the request
header, the appropriate Access-Control-Allow headers should be in the
response.
'''
request_headers = {'Origin': 'http://thirdpartyrequests.org'}
response = self.app.get('/simple_flask', headers=request_headers)
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' in response_headers
nose_tools.assert_equal(response_headers['Access-Control-Allow-Origin'], '*')
nose_tools.assert_equal(response_headers['Access-Control-Allow-Methods'], "POST, PUT, GET, DELETE, OPTIONS")
nose_tools.assert_equal(response_headers['Access-Control-Allow-Headers'], "X-CKAN-API-KEY, Authorization, Content-Type")

@helpers.change_config('ckan.cors.origin_allow_all', 'false')
@helpers.change_config('ckan.site_url', 'http://test.ckan.org')
def test_cors_config_origin_allow_all_false_with_origin_without_whitelist(self):
'''
With origin_allow_all set to false, with an origin in the request
header, but no whitelist defined, there should be no Access-Control-
Allow headers in the response.
'''
request_headers = {'Origin': 'http://thirdpartyrequests.org'}
response = self.app.get('/simple_flask', headers=request_headers)
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' not in response_headers
assert 'Access-Control-Allow-Methods' not in response_headers
assert 'Access-Control-Allow-Headers' not in response_headers

@helpers.change_config('ckan.cors.origin_allow_all', 'false')
@helpers.change_config('ckan.cors.origin_whitelist', 'http://thirdpartyrequests.org')
@helpers.change_config('ckan.site_url', 'http://test.ckan.org')
def test_cors_config_origin_allow_all_false_with_whitelisted_origin(self):
'''
With origin_allow_all set to false, with an origin in the request
header, and a whitelist defined (containing the origin), the
appropriate Access-Control-Allow headers should be in the response.
'''
request_headers = {'Origin': 'http://thirdpartyrequests.org'}
response = self.app.get('/simple_flask', headers=request_headers)
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' in response_headers
nose_tools.assert_equal(response_headers['Access-Control-Allow-Origin'], 'http://thirdpartyrequests.org')
nose_tools.assert_equal(response_headers['Access-Control-Allow-Methods'], "POST, PUT, GET, DELETE, OPTIONS")
nose_tools.assert_equal(response_headers['Access-Control-Allow-Headers'], "X-CKAN-API-KEY, Authorization, Content-Type")

@helpers.change_config('ckan.cors.origin_allow_all', 'false')
@helpers.change_config('ckan.cors.origin_whitelist', 'http://google.com http://thirdpartyrequests.org http://yahoo.co.uk')
@helpers.change_config('ckan.site_url', 'http://test.ckan.org')
def test_cors_config_origin_allow_all_false_with_multiple_whitelisted_origins(self):
'''
With origin_allow_all set to false, with an origin in the request
header, and a whitelist defining multiple allowed origins (containing
the origin), the appropriate Access-Control-Allow headers should be in
the response.
'''
request_headers = {'Origin': 'http://thirdpartyrequests.org'}
response = self.app.get('/simple_flask', headers=request_headers)
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' in response_headers
nose_tools.assert_equal(response_headers['Access-Control-Allow-Origin'], 'http://thirdpartyrequests.org')
nose_tools.assert_equal(response_headers['Access-Control-Allow-Methods'], "POST, PUT, GET, DELETE, OPTIONS")
nose_tools.assert_equal(response_headers['Access-Control-Allow-Headers'], "X-CKAN-API-KEY, Authorization, Content-Type")

@helpers.change_config('ckan.cors.origin_allow_all', 'false')
@helpers.change_config('ckan.cors.origin_whitelist', 'http://google.com http://yahoo.co.uk')
@helpers.change_config('ckan.site_url', 'http://test.ckan.org')
def test_cors_config_origin_allow_all_false_with_whitelist_not_containing_origin(self):
'''
With origin_allow_all set to false, with an origin in the request
header, and a whitelist defining multiple allowed origins (but not
containing the requesting origin), there should be no Access-Control-
Allow headers in the response.
'''
request_headers = {'Origin': 'http://thirdpartyrequests.org'}
response = self.app.get('/simple_flask', headers=request_headers)
response_headers = dict(response.headers)

assert 'Access-Control-Allow-Origin' not in response_headers
assert 'Access-Control-Allow-Methods' not in response_headers
assert 'Access-Control-Allow-Headers' not in response_headers
32 changes: 31 additions & 1 deletion ckan/views/__init__.py
@@ -1,15 +1,45 @@
# encoding: utf-8

from flask import redirect
from pylons import config
from paste.deploy.converters import asbool

import ckan.model as model
from ckan.views.api import APIKEY_HEADER_NAME_DEFAULT
from ckan.common import c, request
import ckan.plugins as p

import logging
log = logging.getLogger(__name__)

APIKEY_HEADER_NAME_KEY = 'apikey_header_name'
APIKEY_HEADER_NAME_DEFAULT = 'X-CKAN-API-Key'


def set_cors_headers_for_response(response):
'''
Set up Access Control Allow headers if either origin_allow_all is True, or
the request Origin is in the origin_whitelist.
'''
if config.get('ckan.cors.origin_allow_all') \
and request.headers.get('Origin'):

cors_origin_allowed = None
if asbool(config.get('ckan.cors.origin_allow_all')):
cors_origin_allowed = "*"
elif config.get('ckan.cors.origin_whitelist') and \
request.headers.get('Origin') \
in config['ckan.cors.origin_whitelist'].split(" "):
# set var to the origin to allow it.
cors_origin_allowed = request.headers.get('Origin')

if cors_origin_allowed is not None:
response.headers['Access-Control-Allow-Origin'] = \
cors_origin_allowed
response.headers['Access-Control-Allow-Methods'] = \
"POST, PUT, GET, DELETE, OPTIONS"
response.headers['Access-Control-Allow-Headers'] = \
"X-CKAN-API-KEY, Authorization, Content-Type"


def identify_user():
'''Try to identify the user
Expand Down
3 changes: 0 additions & 3 deletions ckan/views/api.py
Expand Up @@ -23,9 +23,6 @@
}


APIKEY_HEADER_NAME_KEY = 'apikey_header_name'
APIKEY_HEADER_NAME_DEFAULT = 'X-CKAN-API-Key'

API_DEFAULT_VERSION = 3
API_MAX_VERSION = 3

Expand Down

0 comments on commit 77a436e

Please sign in to comment.