Skip to content

Commit f32bb09

Browse files
authored
Vendor cornice and cornice.ext.swagger (#3497)
* Vendor cornice in kinto.core * Remove cornice from dependencies * Vendor cornice.ext.swagger * from kinto.core.cornice * Disable coverage for imported code
1 parent b05eb23 commit f32bb09

39 files changed

+3565
-43
lines changed

constraints.in

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# main dependencies
22
bcrypt
33
colander
4-
cornice
5-
cornice_swagger
64
dockerflow
75
jsonschema
86
jsonpatch

constraints.txt

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,9 @@ certifi==2024.7.4
2525
charset-normalizer==3.3.2
2626
# via requests
2727
colander==2.0
28-
# via
29-
# -r constraints.in
30-
# cornice-swagger
28+
# via -r constraints.in
3129
colorama==0.4.6
3230
# via logging-color-formatter
33-
cornice==6.0.1
34-
# via
35-
# -r constraints.in
36-
# cornice-swagger
37-
cornice-swagger==1.0.1
38-
# via -r constraints.in
3931
coverage==7.6.4
4032
# via pytest-cov
4133
dockerflow==2024.4.2
@@ -45,9 +37,7 @@ execnet==2.0.2
4537
fqdn==1.5.1
4638
# via jsonschema
4739
greenlet==3.1.1
48-
# via
49-
# playwright
50-
# sqlalchemy
40+
# via playwright
5141
hupper==1.12
5242
# via pyramid
5343
idna==3.7
@@ -111,7 +101,6 @@ pyproject-hooks==1.0.0
111101
pyramid==2.0.2
112102
# via
113103
# -r constraints.in
114-
# cornice
115104
# pyramid-mailer
116105
# pyramid-multiauth
117106
# pyramid-tm
@@ -172,7 +161,6 @@ simplejson==3.19.2
172161
six==1.16.0
173162
# via
174163
# bravado-core
175-
# cornice-swagger
176164
# python-dateutil
177165
# rfc3339-validator
178166
soupsieve==2.5
@@ -211,9 +199,7 @@ urllib3==2.2.2
211199
# requests
212200
# sentry-sdk
213201
venusian==3.1.0
214-
# via
215-
# cornice
216-
# pyramid
202+
# via pyramid
217203
waitress==3.0.2
218204
# via
219205
# -r constraints.in

docs/conf.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
("py:class", "str"),
110110
("py:class", "tuple"),
111111
("py:class", "Exception"),
112-
("py:class", "cornice.Service"),
112+
("py:class", "kinto.core.cornice.Service"),
113113
# Member autodoc fails with those:
114114
# kinto.core.resource.schema
115115
("py:class", "Integer"),
@@ -163,7 +163,6 @@ def setup(app):
163163

164164
intersphinx_mapping = {
165165
"colander": ("https://colander.readthedocs.io/en/latest/", None),
166-
"cornice": ("https://cornice.readthedocs.io/en/latest/", None),
167166
"pyramid": ("https://pyramid.readthedocs.io/en/latest/", None),
168167
}
169168

docs/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ sphinx-github-changelog==1.4.0
66
kinto
77
mock==5.1.0
88
webtest==3.0.4
9-
cornice==6.1.0
109
pyramid==2.0.2
1110
python-rapidjson==1.20
1211
SQLAlchemy==2.0.38

docs/troubleshooting.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ You might get some error like::
104104
File "./kinto/__init__.py", line 4, in <module>
105105
import kinto.core
106106
File "./kinto/core/__init__.py", line 5, in <module>
107-
from cornice import Service as CorniceService
107+
from kinto.core.cornice import Service as CorniceService
108108
ImportError: No module named cornice
109109
unable to load app 0 (mountpoint='') (callable not found or import error)
110110

kinto/core/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import tempfile
55

66
import pkg_resources
7-
from cornice import Service as CorniceService
87
from dockerflow import logging as dockerflow_logging
98
from pyramid.settings import aslist
109

1110
from kinto.core import errors, events
11+
from kinto.core.cornice import Service as CorniceService
1212
from kinto.core.initialization import ( # NOQA
1313
initialize,
1414
install_middlewares,
@@ -186,10 +186,10 @@ def add_api_capability(config, identifier, description="", url="", **kw):
186186
config.add_request_method(events.notify_resource_event, name="notify_resource_event")
187187

188188
# Setup cornice.
189-
config.include("cornice")
189+
config.include("kinto.core.cornice")
190190

191191
# Setup cornice api documentation
192-
config.include("cornice_swagger")
192+
config.include("kinto.core.cornice_swagger")
193193

194194
# Per-request transaction.
195195
config.include("pyramid_tm")

kinto/core/cornice/__init__.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
# You can obtain one at http://mozilla.org/MPL/2.0/.
4+
import logging
5+
from functools import partial
6+
7+
from pyramid.events import NewRequest
8+
from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound
9+
from pyramid.security import NO_PERMISSION_REQUIRED
10+
from pyramid.settings import asbool, aslist
11+
12+
from kinto.core.cornice.errors import Errors # NOQA
13+
from kinto.core.cornice.pyramidhook import (
14+
handle_exceptions,
15+
register_resource_views,
16+
register_service_views,
17+
wrap_request,
18+
)
19+
from kinto.core.cornice.renderer import CorniceRenderer
20+
from kinto.core.cornice.service import Service # NOQA
21+
from kinto.core.cornice.util import ContentTypePredicate, current_service
22+
23+
24+
logger = logging.getLogger("cornice")
25+
26+
27+
def set_localizer_for_languages(event, available_languages, default_locale_name):
28+
"""
29+
Sets the current locale based on the incoming Accept-Language header, if
30+
present, and sets a localizer attribute on the request object based on
31+
the current locale.
32+
33+
To be used as an event handler, this function needs to be partially applied
34+
with the available_languages and default_locale_name arguments. The
35+
resulting function will be an event handler which takes an event object as
36+
its only argument.
37+
"""
38+
request = event.request
39+
if request.accept_language:
40+
accepted = request.accept_language.lookup(available_languages, default=default_locale_name)
41+
request._LOCALE_ = accepted
42+
43+
44+
def setup_localization(config):
45+
"""
46+
Setup localization based on the available_languages and
47+
pyramid.default_locale_name settings.
48+
49+
These settings are named after suggestions from the "Internationalization
50+
and Localization" section of the Pyramid documentation.
51+
"""
52+
try:
53+
config.add_translation_dirs("colander:locale/")
54+
settings = config.get_settings()
55+
available_languages = aslist(settings["available_languages"])
56+
default_locale_name = settings.get("pyramid.default_locale_name", "en")
57+
set_localizer = partial(
58+
set_localizer_for_languages,
59+
available_languages=available_languages,
60+
default_locale_name=default_locale_name,
61+
)
62+
config.add_subscriber(set_localizer, NewRequest)
63+
except ImportError: # pragma: no cover
64+
# add_translation_dirs raises an ImportError if colander is not
65+
# installed
66+
pass
67+
68+
69+
def includeme(config):
70+
"""Include the Cornice definitions"""
71+
# attributes required to maintain services
72+
config.registry.cornice_services = {}
73+
74+
settings = config.get_settings()
75+
76+
# localization request subscriber must be set before first call
77+
# for request.localizer (in wrap_request)
78+
if settings.get("available_languages"):
79+
setup_localization(config)
80+
81+
config.add_directive("add_cornice_service", register_service_views)
82+
config.add_directive("add_cornice_resource", register_resource_views)
83+
config.add_subscriber(wrap_request, NewRequest)
84+
config.add_renderer("cornicejson", CorniceRenderer())
85+
config.add_view_predicate("content_type", ContentTypePredicate)
86+
config.add_request_method(current_service, reify=True)
87+
88+
if asbool(settings.get("handle_exceptions", True)):
89+
config.add_view(handle_exceptions, context=Exception, permission=NO_PERMISSION_REQUIRED)
90+
config.add_view(handle_exceptions, context=HTTPNotFound, permission=NO_PERMISSION_REQUIRED)
91+
config.add_view(
92+
handle_exceptions, context=HTTPForbidden, permission=NO_PERMISSION_REQUIRED
93+
)

kinto/core/cornice/cors.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
# You can obtain one at http://mozilla.org/MPL/2.0/.
4+
import fnmatch
5+
import functools
6+
7+
from pyramid.settings import asbool
8+
9+
10+
CORS_PARAMETERS = (
11+
"cors_headers",
12+
"cors_enabled",
13+
"cors_origins",
14+
"cors_credentials",
15+
"cors_max_age",
16+
"cors_expose_all_headers",
17+
)
18+
19+
20+
def get_cors_preflight_view(service):
21+
"""Return a view for the OPTION method.
22+
23+
Checks that the User-Agent is authorized to do a request to the server, and
24+
to this particular service, and add the various checks that are specified
25+
in http://www.w3.org/TR/cors/#resource-processing-model.
26+
"""
27+
28+
def _preflight_view(request):
29+
response = request.response
30+
origin = request.headers.get("Origin")
31+
supported_headers = service.cors_supported_headers_for()
32+
33+
if not origin:
34+
request.errors.add("header", "Origin", "this header is mandatory")
35+
36+
requested_method = request.headers.get("Access-Control-Request-Method")
37+
if not requested_method:
38+
request.errors.add(
39+
"header", "Access-Control-Request-Method", "this header is mandatory"
40+
)
41+
42+
if not (requested_method and origin):
43+
return
44+
45+
requested_headers = request.headers.get("Access-Control-Request-Headers", ())
46+
47+
if requested_headers:
48+
requested_headers = map(str.strip, requested_headers.split(","))
49+
50+
if requested_method not in service.cors_supported_methods:
51+
request.errors.add("header", "Access-Control-Request-Method", "Method not allowed")
52+
53+
if not service.cors_expose_all_headers:
54+
for h in requested_headers:
55+
if h.lower() not in [s.lower() for s in supported_headers]:
56+
request.errors.add(
57+
"header", "Access-Control-Request-Headers", 'Header "%s" not allowed' % h
58+
)
59+
60+
supported_headers = set(supported_headers) | set(requested_headers)
61+
62+
response.headers["Access-Control-Allow-Headers"] = ",".join(supported_headers)
63+
64+
response.headers["Access-Control-Allow-Methods"] = ",".join(service.cors_supported_methods)
65+
66+
max_age = service.cors_max_age_for(requested_method)
67+
if max_age is not None:
68+
response.headers["Access-Control-Max-Age"] = str(max_age)
69+
70+
return None
71+
72+
return _preflight_view
73+
74+
75+
def _get_method(request):
76+
"""Return what's supposed to be the method for CORS operations.
77+
(e.g if the verb is options, look at the A-C-Request-Method header,
78+
otherwise return the HTTP verb).
79+
"""
80+
if request.method == "OPTIONS":
81+
method = request.headers.get("Access-Control-Request-Method", request.method)
82+
else:
83+
method = request.method
84+
return method
85+
86+
87+
def ensure_origin(service, request, response=None, **kwargs):
88+
"""Ensure that the origin header is set and allowed."""
89+
response = response or request.response
90+
91+
# Don't check this twice.
92+
if not request.info.get("cors_checked", False):
93+
method = _get_method(request)
94+
95+
origin = request.headers.get("Origin")
96+
97+
if not origin:
98+
always_cors = asbool(request.registry.settings.get("cornice.always_cors"))
99+
# With this setting, if the service origins has "*", then
100+
# always return CORS headers.
101+
origins = getattr(service, "cors_origins", [])
102+
if always_cors and "*" in origins:
103+
origin = "*"
104+
105+
if origin:
106+
if not any([fnmatch.fnmatchcase(origin, o) for o in service.cors_origins_for(method)]):
107+
request.errors.add("header", "Origin", "%s not allowed" % origin)
108+
elif service.cors_support_credentials_for(method):
109+
response.headers["Access-Control-Allow-Origin"] = origin
110+
else:
111+
if any([o == "*" for o in service.cors_origins_for(method)]):
112+
response.headers["Access-Control-Allow-Origin"] = "*"
113+
else:
114+
response.headers["Access-Control-Allow-Origin"] = origin
115+
request.info["cors_checked"] = True
116+
return response
117+
118+
119+
def get_cors_validator(service):
120+
return functools.partial(ensure_origin, service)
121+
122+
123+
def apply_cors_post_request(service, request, response):
124+
"""Handles CORS-related post-request things.
125+
126+
Add some response headers, such as the Expose-Headers and the
127+
Allow-Credentials ones.
128+
"""
129+
response = ensure_origin(service, request, response)
130+
method = _get_method(request)
131+
132+
if (
133+
service.cors_support_credentials_for(method)
134+
and "Access-Control-Allow-Credentials" not in response.headers
135+
):
136+
response.headers["Access-Control-Allow-Credentials"] = "true"
137+
138+
if request.method != "OPTIONS":
139+
# Which headers are exposed?
140+
supported_headers = service.cors_supported_headers_for(request.method)
141+
if supported_headers:
142+
response.headers["Access-Control-Expose-Headers"] = ", ".join(supported_headers)
143+
144+
return response

0 commit comments

Comments
 (0)