You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by je...@apache.org on 2015/07/16 17:56:52 UTC
[1/2] allura git commit: [#7927] ticket:821 Implement CORS middleware
Repository: allura
Updated Branches:
refs/heads/ib/7927 [created] 6625703b1
[#7927] ticket:821 Implement CORS middleware
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/0c71798b
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/0c71798b
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/0c71798b
Branch: refs/heads/ib/7927
Commit: 0c71798b2dbe73f68f5912a81606b9ca73f45704
Parents: de47627
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Jul 16 15:54:22 2015 +0300
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Thu Jul 16 17:50:54 2015 +0300
----------------------------------------------------------------------
Allura/allura/config/middleware.py | 9 ++++-
Allura/allura/lib/custom_middleware.py | 57 +++++++++++++++++++++++++++++
Allura/development.ini | 9 +++++
3 files changed, 74 insertions(+), 1 deletion(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/0c71798b/Allura/allura/config/middleware.py
----------------------------------------------------------------------
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index ba07cc4..922034d 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -25,7 +25,7 @@ import tg
import tg.error
import pkg_resources
from tg import config
-from paste.deploy.converters import asbool
+from paste.deploy.converters import asbool, aslist, asint
from paste.registry import RegistryManager
from routes.middleware import RoutesMiddleware
from pylons.middleware import StatusCodeRedirect
@@ -44,6 +44,7 @@ from allura.lib.custom_middleware import AlluraTimerMiddleware
from allura.lib.custom_middleware import SSLMiddleware
from allura.lib.custom_middleware import StaticFilesMiddleware
from allura.lib.custom_middleware import CSRFMiddleware
+from allura.lib.custom_middleware import CORSMiddleware
from allura.lib.custom_middleware import LoginRedirectMiddleware
from allura.lib.custom_middleware import RememberLoginMiddleware
from allura.lib import patches
@@ -137,6 +138,12 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
# Clear cookies when the CSRF field isn't posted
if not app_conf.get('disable_csrf_protection'):
app = CSRFMiddleware(app, '_session_id')
+ if asbool(config.get('cors.enabled', False)):
+ # Handle CORS requests
+ allowed_methods = aslist(config.get('cors.methods'))
+ allowed_headers = aslist(config.get('cors.headers'))
+ cache_duration = asint(config.get('cors.cache_duration', 0))
+ app = CORSMiddleware(app, allowed_methods, allowed_headers)
# Setup the allura SOPs
app = allura_globals_middleware(app)
# Ensure http and https used per config
http://git-wip-us.apache.org/repos/asf/allura/blob/0c71798b/Allura/allura/lib/custom_middleware.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index 13c3db5..4eef689 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -84,6 +84,63 @@ class StaticFilesMiddleware(object):
return fileapp.FileApp(file_path, [
('Access-Control-Allow-Origin', '*')])
+class CORSMiddleware(object):
+ '''Enables Cross-Origin Resource Sharing for REST API'''
+
+ def __init__(self, app, allowed_methods, allowed_headers, cache=None):
+ self.app = app
+ self.allowed_methods = [m.upper() for m in allowed_methods]
+ self.allowed_headers = set(h.lower() for h in allowed_headers)
+ self.cache_preflight = cache or None
+
+ def __call__(self, environ, start_response):
+ is_api_request = environ.get('PATH_INFO', '').startswith('/rest/')
+ valid_cors = 'HTTP_ORIGIN' in environ
+ if not is_api_request or not valid_cors:
+ return self.app(environ, start_response)
+
+ method = environ.get('REQUEST_METHOD')
+ acrm = environ.get('HTTP_ACCESS_CONTROL_REQUEST_METHOD')
+ if method == 'OPTIONS' and acrm:
+ return self.handle_preflight_request(environ, start_response)
+ else:
+ return self.handle_simple_request(environ, start_response)
+
+ def handle_simple_request(self, environ, start_response):
+ def cors_start_response(status, headers, exc_info=None):
+ headers.extend(self.get_response_headers(preflight=False))
+ return start_response(status, headers, exc_info)
+ return self.app(environ, cors_start_response)
+
+ def handle_preflight_request(self, environ, start_response):
+ method = environ.get('HTTP_ACCESS_CONTROL_REQUEST_METHOD')
+ if method not in self.allowed_methods:
+ return self.app(environ, start_response)
+ headers = self.get_access_control_request_headers(environ)
+ if not headers.issubset(self.allowed_headers):
+ return self.app(environ, start_response)
+ r = exc.HTTPOk(headers=self.get_response_headers(preflight=True))
+ return r(environ, start_response)
+
+ def get_response_headers(self, preflight=False):
+ headers = [('Access-Control-Allow-Origin', '*')]
+ if preflight:
+ ac_methods = ', '.join(self.allowed_methods)
+ ac_headers = ', '.join(self.allowed_headers)
+ headers.extend([
+ ('Access-Control-Allow-Methods', ac_methods),
+ ('Access-Control-Allow-Headers', ac_headers),
+ ])
+ if self.cache_preflight:
+ headers.append(
+ ('Access-Control-Max-Age', self.cache_preflight)
+ )
+ return headers
+
+ def get_access_control_request_headers(self, environ):
+ headers = environ.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', '')
+ return set(h.strip().lower() for h in headers.split(',') if h.strip())
+
class LoginRedirectMiddleware(object):
http://git-wip-us.apache.org/repos/asf/allura/blob/0c71798b/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 02d3e9f..934ea67 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -216,6 +216,15 @@ webhook.repo_push.limit = 30
; `WebhookSender.triggered_by`) and values are actual limits (default=3), e.g.:
webhook.repo_push.max_hooks = {"git": 3, "hg": 3, "svn": 3}
+;; Allow Cross-Origin Resource Sharing (CORS) requests to the REST API
+; disabled by default, uncomment the following options to enable:
+;cors.enabled = true
+;cors.methods = GET HEAD POST PUT DELETE
+;cors.headers = Authorization Accept Content-Type
+; Allow clients to cache preflight responses for N seconds
+; Set to 0 or remove completely to disable
+;cors.cache_duration = 86400
+
; Additional fields for admin project/user search
; Note: whitespace after comma is important!
;search.project.additional_search_fields = private, url, title
[2/2] allura git commit: [#7927] ticket:821 Add tests for
CORSMiddleware
Posted by je...@apache.org.
[#7927] ticket:821 Add tests for CORSMiddleware
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/6625703b
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/6625703b
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/6625703b
Branch: refs/heads/ib/7927
Commit: 6625703b123d070d813f6ddf0dce05de729e15b2
Parents: 0c71798
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Jul 16 18:01:13 2015 +0300
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Thu Jul 16 18:01:13 2015 +0300
----------------------------------------------------------------------
Allura/allura/tests/test_middlewares.py | 106 +++++++++++++++++++++++++++
1 file changed, 106 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/6625703b/Allura/allura/tests/test_middlewares.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
new file mode 100644
index 0000000..1ca6d8a
--- /dev/null
+++ b/Allura/allura/tests/test_middlewares.py
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from mock import MagicMock, patch
+from datadiff.tools import assert_equal
+from nose.tools import assert_not_equal
+from allura.lib.custom_middleware import CORSMiddleware
+
+
+class TestCORSMiddleware(object):
+
+ def setUp(self):
+ self.app = MagicMock()
+ self.allowed_methods = ['GET', 'POST', 'DELETE']
+ self.allowed_headers = ['Authorization', 'Accept']
+ self.cors = CORSMiddleware(
+ self.app,
+ self.allowed_methods,
+ self.allowed_headers)
+
+ def test_init(self):
+ cors = CORSMiddleware(self.app, ['get', 'post'], ['Some-Header'])
+ assert_equal(cors.app, self.app)
+ assert_equal(cors.allowed_methods, ['GET', 'POST'])
+ assert_equal(cors.allowed_headers, set(['some-header']))
+
+ def test_call_not_api_request(self):
+ callback = MagicMock()
+ env = {'PATH_INFO': '/p/test/'}
+ self.cors(env, callback)
+ self.app.assert_called_once_with(env, callback)
+
+ def test_call_invalid_cors(self):
+ callback = MagicMock()
+ env = {'PATH_INFO': '/rest/p/test/'}
+ self.cors(env, callback)
+ self.app.assert_called_once_with(env, callback)
+
+ def test_handle_call_simple_request(self):
+ callback = MagicMock()
+ env = {'PATH_INFO': '/rest/p/test/',
+ 'HTTP_ORIGIN': 'my.site.com',
+ 'REQUEST_METHOD': 'GET'}
+ self.cors(env, callback)
+ assert_equal(self.app.call_count, 1)
+ assert_equal(self.app.call_args_list[0][0][0], env)
+ assert_not_equal(self.app.call_args_list[0][0][1], callback)
+
+ @patch('allura.lib.custom_middleware.exc', autospec=True)
+ def test_handle_call_preflight_request(self, exc):
+ callback = MagicMock()
+ env = {'PATH_INFO': '/rest/p/test/',
+ 'HTTP_ORIGIN': 'my.site.com',
+ 'REQUEST_METHOD': 'OPTIONS',
+ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD': 'POST'}
+ self.cors(env, callback)
+ assert_equal(self.app.call_count, 0)
+ exc.HTTPOk.assert_called_once_with(headers=[
+ ('Access-Control-Allow-Origin', '*'),
+ ('Access-Control-Allow-Methods', 'GET, POST, DELETE'),
+ ('Access-Control-Allow-Headers', 'accept, authorization')
+ ])
+ exc.HTTPOk.return_value.assert_called_once_with(env, callback)
+
+ def test_get_response_headers_simple(self):
+ assert_equal(self.cors.get_response_headers(),
+ [('Access-Control-Allow-Origin', '*')])
+ assert_equal(self.cors.get_response_headers(preflight=False),
+ [('Access-Control-Allow-Origin', '*')])
+
+ def test_get_response_headers_preflight(self):
+ assert_equal(
+ self.cors.get_response_headers(preflight=True),
+ [('Access-Control-Allow-Origin', '*'),
+ ('Access-Control-Allow-Methods', 'GET, POST, DELETE'),
+ ('Access-Control-Allow-Headers', 'accept, authorization')])
+
+ def test_get_response_headers_preflight_with_cache(self):
+ cors = CORSMiddleware(self.app, ['GET', 'PUT'], ['Accept'], 86400)
+ assert_equal(cors.get_response_headers(preflight=True),
+ [('Access-Control-Allow-Origin', '*'),
+ ('Access-Control-Allow-Methods', 'GET, PUT'),
+ ('Access-Control-Allow-Headers', 'accept'),
+ ('Access-Control-Max-Age', 86400)])
+
+ def test_get_access_control_request_headers(self):
+ key = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
+ f = self.cors.get_access_control_request_headers
+ assert_equal(f({}), set())
+ assert_equal(f({key: ''}), set())
+ assert_equal(f({key: 'Authorization, Accept'}),
+ set(['authorization', 'accept']))