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']))