Commit 424d55d1 authored by Nicolas Chauvat's avatar Nicolas Chauvat
Browse files

remove web.cors in favor of wsgicors with pyramid

parent f81fd647ea03
Pipeline #29236 failed with stages
in 35 minutes and 51 seconds
......@@ -129,8 +129,6 @@ def wsgi_application_from_cwconfig(
config.add_view(debug_display_source_code, route_name='debug_display_source_code')
app = config.make_wsgi_app()
# This replaces completely web/cors.py, which is not used by
# cubicweb.pyramid anymore
app = wsgicors.CORS(
app,
origin=' '.join(cwconfig['access-control-allow-origin']),
......
......@@ -32,7 +32,6 @@ from cubicweb import (
CW_EVENT_MANAGER, ValidationError, Unauthorized, Forbidden,
AuthenticationError, NoSelectableObject)
from cubicweb.repoapi import anonymous_cnx
from cubicweb.web import cors
from cubicweb.web import (
LOGGER, DirectResponse, Redirect, NotFound, LogOut,
RemoteCallFailed, InvalidSession, RequestError, PublishException)
......@@ -341,8 +340,6 @@ class CubicWebPublisher(object):
try:
# standard processing of the request
try:
# apply CORS sanity checks
cors.process_request(req, self.vreg.config)
ctrlid, rset = self.url_resolver.process(req, path)
try:
controller = self.vreg['controllers'].select(ctrlid, req,
......@@ -351,10 +348,6 @@ class CubicWebPublisher(object):
raise Unauthorized(req._('not authorized'))
req.update_search_state()
result = controller.publish(rset=rset)
except cors.CORSPreflight:
# Return directly an empty 200
req.status_out = 200
result = b''
except Redirect as ex:
# Redirect may be raised by edit controller when everything went
# fine, so attempt to commit
......
# copyright 2014-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""A set of utility functions to handle CORS requests
Unless specified, all references in this file are related to:
http://www.w3.org/TR/cors
The provided implementation roughly follows:
http://www.html5rocks.com/static/images/cors_server_flowchart.png
See also:
https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
"""
from urllib.parse import urlsplit
from cubicweb.web import LOGGER
info = LOGGER.info
class CORSFailed(Exception):
"""Raised when cross origin resource sharing checks failed"""
class CORSPreflight(Exception):
"""Raised when cross origin resource sharing checks detects the
request as a valid preflight request"""
def process_request(req, config):
"""
Process a request to apply CORS specification algorithms
Check whether the CORS specification is respected and set corresponding
headers to ensure response complies with the specification.
In case of non-compliance, no CORS-related header is set.
"""
base_url = urlsplit(req.base_url())
expected_host = '://'.join((base_url.scheme, base_url.netloc))
if not req.get_header('Origin') or req.get_header('Origin') == expected_host:
# not a CORS request, nothing to do
return
try:
# handle cross origin resource sharing (CORS)
if req.http_method() == 'OPTIONS':
if req.get_header('Access-Control-Request-Method'):
# preflight CORS request
process_preflight(req, config)
else: # Simple CORS or actual request
process_simple(req, config)
except CORSFailed as exc:
info('Cross origin resource sharing failed: %s' % exc)
except CORSPreflight:
info('Cross origin resource sharing: valid Preflight request %s')
raise
def process_preflight(req, config):
"""cross origin resource sharing (preflight)
Cf http://www.w3.org/TR/cors/#resource-preflight-requests
"""
origin = check_origin(req, config)
allowed_methods = set(config['access-control-allow-methods'])
allowed_headers = set(config['access-control-allow-headers'])
try:
method = req.get_header('Access-Control-Request-Method')
except ValueError:
raise CORSFailed('Access-Control-Request-Method is incorrect')
if method not in allowed_methods:
raise CORSFailed('Method is not allowed')
try:
req.get_header('Access-Control-Request-Headers', ())
except ValueError:
raise CORSFailed('Access-Control-Request-Headers is incorrect')
req.set_header('Access-Control-Allow-Methods', allowed_methods, raw=False)
req.set_header('Access-Control-Allow-Headers', allowed_headers, raw=False)
process_common(req, config, origin)
raise CORSPreflight()
def process_simple(req, config):
"""Handle the Simple Cross-Origin Request case
"""
origin = check_origin(req, config)
exposed_headers = config['access-control-expose-headers']
if exposed_headers:
req.set_header('Access-Control-Expose-Headers', exposed_headers, raw=False)
process_common(req, config, origin)
def process_common(req, config, origin):
req.set_header('Access-Control-Allow-Origin', origin)
# in CW, we always support credential/authentication
req.set_header('Access-Control-Allow-Credentials', 'true')
def check_origin(req, config):
origin = req.get_header('Origin').lower()
allowed_origins = config.get('access-control-allow-origin')
if not allowed_origins:
raise CORSFailed('access-control-allow-origin is not configured')
if '*' not in allowed_origins and origin not in allowed_origins:
raise CORSFailed('Origin is not allowed')
# bit of sanity check; see "6.3 Security"
myhost = urlsplit(req.base_url()).netloc
host = req.get_header('Host')
if host != myhost:
info('cross origin resource sharing detected possible '
'DNS rebinding attack Host header != host of base_url: '
'%s != %s' % (host, myhost))
raise CORSFailed('Host header and hostname do not match')
# include "Vary: Origin" header (see 6.4)
req.headers_out.addHeader('Vary', 'Origin')
return origin
......@@ -258,166 +258,5 @@ class HTTPCache(TestCase):
req = _test_cache(hin, hout, method='POST')
self.assertCache(412, req.status_out, 'not modifier HEAD verb')
alloworig = 'access-control-allow-origin'
allowmethods = 'access-control-allow-methods'
allowheaders = 'access-control-allow-headers'
allowcreds = 'access-control-allow-credentials'
exposeheaders = 'access-control-expose-headers'
maxage = 'access-control-max-age'
requestmethod = 'access-control-request-method'
requestheaders = 'access-control-request-headers'
class _BaseAccessHeadersTC(CubicWebTC):
@contextlib.contextmanager
def options(self, **options):
for k, values in options.items():
self.config.set_option(k, values)
try:
yield
finally:
for k in options:
self.config.set_option(k, '')
def check_no_cors(self, req):
self.assertEqual(None, req.get_response_header(alloworig))
self.assertEqual(None, req.get_response_header(allowmethods))
self.assertEqual(None, req.get_response_header(allowheaders))
self.assertEqual(None, req.get_response_header(allowcreds))
self.assertEqual(None, req.get_response_header(exposeheaders))
self.assertEqual(None, req.get_response_header(maxage))
class SimpleAccessHeadersTC(_BaseAccessHeadersTC):
def test_noaccess(self):
with self.admin_access.web_request() as req:
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_noorigin(self):
with self.options(**{alloworig: '*'}):
with self.admin_access.web_request() as req:
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_origin_noaccess(self):
with self.admin_access.web_request() as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_origin_noaccess_bad_host(self):
with self.options(**{alloworig: '*'}):
with self.admin_access.web_request() as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'badhost.net')
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_explicit_origin_noaccess(self):
with self.options(**{alloworig: ['http://www.toto.org', 'http://othersite.fr']}):
with self.admin_access.web_request() as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'testing.fr')
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_origin_access(self):
with self.options(**{alloworig: '*'}):
with self.admin_access.web_request() as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'testing.fr')
data = self.app_handle_request(req)
self.assertEqual('http://www.cubicweb.org',
req.get_response_header(alloworig))
def test_explicit_origin_access(self):
with self.options(**{alloworig: ['http://www.cubicweb.org', 'http://othersite.fr']}):
with self.admin_access.web_request() as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'testing.fr')
data = self.app_handle_request(req)
self.assertEqual('http://www.cubicweb.org',
req.get_response_header(alloworig))
def test_origin_access_headers(self):
with self.options(**{alloworig: '*',
exposeheaders: ['ExposeHead1', 'ExposeHead2'],
allowheaders: ['AllowHead1', 'AllowHead2'],
allowmethods: ['GET', 'POST', 'OPTIONS']}):
with self.admin_access.web_request() as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'testing.fr')
data = self.app_handle_request(req)
self.assertEqual('http://www.cubicweb.org',
req.get_response_header(alloworig))
self.assertEqual("true",
req.get_response_header(allowcreds))
self.assertEqual(['ExposeHead1', 'ExposeHead2'],
req.get_response_header(exposeheaders))
self.assertEqual(None, req.get_response_header(allowmethods))
self.assertEqual(None, req.get_response_header(allowheaders))
class PreflightAccessHeadersTC(_BaseAccessHeadersTC):
def test_noaccess(self):
with self.admin_access.web_request(method='OPTIONS') as req:
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_noorigin(self):
with self.options(**{alloworig: '*'}):
with self.admin_access.web_request(method='OPTIONS') as req:
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_origin_noaccess(self):
with self.admin_access.web_request(method='OPTIONS') as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_origin_noaccess_bad_host(self):
with self.options(**{alloworig: '*'}):
with self.admin_access.web_request(method='OPTIONS') as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'badhost.net')
data = self.app_handle_request(req)
self.check_no_cors(req)
def test_origin_access(self):
with self.options(**{alloworig: '*',
exposeheaders: ['ExposeHead1', 'ExposeHead2'],
allowheaders: ['AllowHead1', 'AllowHead2'],
allowmethods: ['GET', 'POST', 'OPTIONS']}):
with self.admin_access.web_request(method='OPTIONS') as req:
req.set_request_header('Origin', 'http://www.cubicweb.org')
# in these tests, base_url is http://testing.fr/cubicweb/
req.set_request_header('Host', 'testing.fr')
req.set_request_header(requestmethod, 'GET')
data = self.app_handle_request(req)
self.assertEqual(200, req.status_out)
self.assertEqual('http://www.cubicweb.org',
req.get_response_header(alloworig))
self.assertEqual("true",
req.get_response_header(allowcreds))
self.assertEqual(set(['GET', 'POST', 'OPTIONS']),
req.get_response_header(allowmethods))
self.assertEqual(set(['AllowHead1', 'AllowHead2']),
req.get_response_header(allowheaders))
self.assertEqual(None,
req.get_response_header(exposeheaders))
if __name__ == '__main__':
unittest_main()
......@@ -155,23 +155,16 @@ RQL queries.
Cross-Origin Resource Sharing
-----------------------------
CubicWeb provides some support for the CORS_ protocol. For now, the
provided implementation only deals with access to a CubicWeb instance
as a whole. Support for a finer granularity may be considered in the
future.
CubicWeb's support for the CORS_ protocol is provided by the wsgicors_
middleware at the Pyramid level. For now, the provided implementation only
deals with access to a CubicWeb instance as a whole. Support for a finer
granularity may be considered in the future.
Specificities of the provided implementation:
.. _wsgicors: https://pypi.org/project/wsgicors/
- ``Access-Control-Allow-Credentials`` is always true
- ``Access-Control-Allow-Origin`` header in response will never be
``*``
- ``Access-Control-Expose-Headers`` can be configured globally (see below)
- ``Access-Control-Max-Age`` can be configured globally (see below)
- ``Access-Control-Allow-Methods`` can be configured globally (see below)
- ``Access-Control-Allow-Headers`` can be configured globally (see below)
A few parameters can be set to configure the CORS_ capabilities of CubicWeb.
A few parameters can be set to configure the CORS_ capabilities of CubicWeb,
the values are passed to the `wsgicors.CORS()` middleware constructor, hence
the wsgicors documentation can be used for more details.
.. _CORS: http://www.w3.org/TR/cors/
......@@ -179,10 +172,11 @@ A few parameters can be set to configure the CORS_ capabilities of CubicWeb.
comma-separated list of allowed origin domains or "*" for any domain
:`access-control-allow-methods`:
comma-separated list of allowed HTTP methods
:`access-control-max-age`:
maximum age of cross-origin resource sharing (in seconds)
:`access-control-allow-headers`:
comma-separated list of allowed HTTP custom headers (used in simple requests)
:`access-control-expose-headers`:
comma-separated list of allowed HTTP custom headers (used in preflight requests)
:`access-control-max-age`:
maximum age of cross-origin resource sharing (in seconds)
`credentials` is always set to `true` and is not configurable.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment