You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2015/07/21 19:44:49 UTC
[1/4] allura git commit: [#7927] ticket:821 Implement CORS middleware
Repository: allura
Updated Branches:
refs/heads/master de47627f7 -> bbeebdbb9
[#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/master
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
[3/4] allura git commit: [#7927] pass cache setting through;
use str in headers
Posted by br...@apache.org.
[#7927] pass cache setting through; use str in headers
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/82a3642b
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/82a3642b
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/82a3642b
Branch: refs/heads/master
Commit: 82a3642b3c36839e7fcbbc9ebce43fad4eb6a531
Parents: 6625703
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Tue Jul 21 17:42:55 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Tue Jul 21 17:44:31 2015 +0000
----------------------------------------------------------------------
Allura/allura/config/middleware.py | 2 +-
Allura/allura/lib/custom_middleware.py | 2 +-
Allura/allura/tests/test_middlewares.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/82a3642b/Allura/allura/config/middleware.py
----------------------------------------------------------------------
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 922034d..6c39d4b 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -143,7 +143,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf):
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)
+ app = CORSMiddleware(app, allowed_methods, allowed_headers, cache_duration)
# 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/82a3642b/Allura/allura/lib/custom_middleware.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index 4eef689..e56a530 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -133,7 +133,7 @@ class CORSMiddleware(object):
])
if self.cache_preflight:
headers.append(
- ('Access-Control-Max-Age', self.cache_preflight)
+ ('Access-Control-Max-Age', str(self.cache_preflight))
)
return headers
http://git-wip-us.apache.org/repos/asf/allura/blob/82a3642b/Allura/allura/tests/test_middlewares.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index 1ca6d8a..4b45097 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -95,7 +95,7 @@ class TestCORSMiddleware(object):
[('Access-Control-Allow-Origin', '*'),
('Access-Control-Allow-Methods', 'GET, PUT'),
('Access-Control-Allow-Headers', 'accept'),
- ('Access-Control-Max-Age', 86400)])
+ ('Access-Control-Max-Age', '86400')])
def test_get_access_control_request_headers(self):
key = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
[4/4] allura git commit: [#7927] strengthen / document tests a bit
Posted by br...@apache.org.
[#7927] strengthen / document tests a bit
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/bbeebdbb
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/bbeebdbb
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/bbeebdbb
Branch: refs/heads/master
Commit: bbeebdbb9a3059f1c4595203967e2dbb6bd3a7a6
Parents: 82a3642
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Tue Jul 21 17:43:23 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Tue Jul 21 17:44:31 2015 +0000
----------------------------------------------------------------------
Allura/allura/tests/functional/test_rest.py | 18 ++++++++++++++++++
Allura/allura/tests/test_middlewares.py | 1 +
2 files changed, 19 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/bbeebdbb/Allura/allura/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index 3fed49c..903cda7 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -353,6 +353,24 @@ class TestRestHome(TestRestApiBase):
r = self.api_get('/rest/p/test/')
assert r.status_int == 404
+ @td.with_wiki
+ def test_cors_POST_req_blocked_by_csrf(self):
+ # so test-admin isn't automatically logged in for all requests
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
+
+ # regular login to get a session cookie set up
+ r = self.app.get('/auth/')
+ r.form['username'] = 'test-admin'
+ r.form['password'] = 'foo'
+ r.form.submit()
+
+ # simulate CORS ajax request withCredentials (cookie headers)
+ # make sure we don't allow the cookies to authorize the request (else could be a CSRF attack vector)
+ assert self.app.cookies['allura']
+ self.app.post('/rest/p/test/wiki/NewPage', headers={'Origin': 'http://bad.com/'},
+ status=401)
+
+
class TestDoap(TestRestApiBase):
validate_skip = True
ns = '{http://usefulinc.com/ns/doap#}'
http://git-wip-us.apache.org/repos/asf/allura/blob/bbeebdbb/Allura/allura/tests/test_middlewares.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index 4b45097..58dc515 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -77,6 +77,7 @@ class TestCORSMiddleware(object):
exc.HTTPOk.return_value.assert_called_once_with(env, callback)
def test_get_response_headers_simple(self):
+ # Allow-Origin: * is crucial for security, since that prevents browsers from exposing results fetched withCredentials: true (aka cookies)
assert_equal(self.cors.get_response_headers(),
[('Access-Control-Allow-Origin', '*')])
assert_equal(self.cors.get_response_headers(preflight=False),
[2/4] allura git commit: [#7927] ticket:821 Add tests for
CORSMiddleware
Posted by br...@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/master
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']))