kinto 19.5.0__py3-none-any.whl → 19.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kinto might be problematic. Click here for more details.
- kinto/core/__init__.py +3 -3
- kinto/core/cornice/__init__.py +93 -0
- kinto/core/cornice/cors.py +144 -0
- kinto/core/cornice/errors.py +40 -0
- kinto/core/cornice/pyramidhook.py +373 -0
- kinto/core/cornice/renderer.py +89 -0
- kinto/core/cornice/resource.py +205 -0
- kinto/core/cornice/service.py +641 -0
- kinto/core/cornice/util.py +138 -0
- kinto/core/cornice/validators/__init__.py +94 -0
- kinto/core/cornice/validators/_colander.py +142 -0
- kinto/core/cornice/validators/_marshmallow.py +182 -0
- kinto/core/cornice_swagger/__init__.py +92 -0
- kinto/core/cornice_swagger/converters/__init__.py +21 -0
- kinto/core/cornice_swagger/converters/exceptions.py +6 -0
- kinto/core/cornice_swagger/converters/parameters.py +90 -0
- kinto/core/cornice_swagger/converters/schema.py +249 -0
- kinto/core/cornice_swagger/swagger.py +725 -0
- kinto/core/cornice_swagger/templates/index.html +73 -0
- kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
- kinto/core/cornice_swagger/util.py +42 -0
- kinto/core/cornice_swagger/views.py +78 -0
- kinto/core/openapi.py +2 -3
- kinto/core/resource/viewset.py +1 -1
- kinto/core/testing.py +1 -1
- kinto/core/utils.py +3 -2
- kinto/core/views/batch.py +1 -1
- kinto/core/views/openapi.py +1 -1
- kinto/plugins/flush.py +1 -1
- kinto/plugins/openid/views.py +1 -1
- kinto/views/contribute.py +2 -1
- {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/METADATA +2 -4
- {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/RECORD +37 -16
- {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/LICENSE +0 -0
- {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/WHEEL +0 -0
- {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/entry_points.txt +0 -0
- {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/top_level.txt +0 -0
kinto/core/__init__.py
CHANGED
|
@@ -4,11 +4,11 @@ import logging
|
|
|
4
4
|
import tempfile
|
|
5
5
|
|
|
6
6
|
import pkg_resources
|
|
7
|
-
from cornice import Service as CorniceService
|
|
8
7
|
from dockerflow import logging as dockerflow_logging
|
|
9
8
|
from pyramid.settings import aslist
|
|
10
9
|
|
|
11
10
|
from kinto.core import errors, events
|
|
11
|
+
from kinto.core.cornice import Service as CorniceService
|
|
12
12
|
from kinto.core.initialization import ( # NOQA
|
|
13
13
|
initialize,
|
|
14
14
|
install_middlewares,
|
|
@@ -186,10 +186,10 @@ def includeme(config):
|
|
|
186
186
|
config.add_request_method(events.notify_resource_event, name="notify_resource_event")
|
|
187
187
|
|
|
188
188
|
# Setup cornice.
|
|
189
|
-
config.include("cornice")
|
|
189
|
+
config.include("kinto.core.cornice")
|
|
190
190
|
|
|
191
191
|
# Setup cornice api documentation
|
|
192
|
-
config.include("cornice_swagger")
|
|
192
|
+
config.include("kinto.core.cornice_swagger")
|
|
193
193
|
|
|
194
194
|
# Per-request transaction.
|
|
195
195
|
config.include("pyramid_tm")
|
|
@@ -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
|
+
)
|
|
@@ -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,0 +1,40 @@
|
|
|
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 json
|
|
5
|
+
|
|
6
|
+
from pyramid.i18n import TranslationString
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Errors(list):
|
|
10
|
+
"""Holds Request errors"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, status=400, localizer=None):
|
|
13
|
+
self.status = status
|
|
14
|
+
self.localizer = localizer
|
|
15
|
+
super(Errors, self).__init__()
|
|
16
|
+
|
|
17
|
+
def add(self, location, name=None, description=None, **kw):
|
|
18
|
+
"""Registers a new error."""
|
|
19
|
+
allowed = ("body", "querystring", "url", "header", "path", "cookies", "method")
|
|
20
|
+
if location != "" and location not in allowed:
|
|
21
|
+
raise ValueError("%r not in %s" % (location, allowed))
|
|
22
|
+
|
|
23
|
+
if isinstance(description, TranslationString) and self.localizer:
|
|
24
|
+
description = self.localizer.translate(description)
|
|
25
|
+
|
|
26
|
+
self.append(dict(location=location, name=name, description=description, **kw))
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_json(cls, string):
|
|
30
|
+
"""Transforms a json string into an `Errors` instance"""
|
|
31
|
+
obj = json.loads(string.decode())
|
|
32
|
+
return Errors.from_list(obj.get("errors", []))
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_list(cls, obj):
|
|
36
|
+
"""Transforms a python list into an `Errors` instance"""
|
|
37
|
+
errors = Errors()
|
|
38
|
+
for error in obj:
|
|
39
|
+
errors.add(**error)
|
|
40
|
+
return errors
|
|
@@ -0,0 +1,373 @@
|
|
|
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 copy
|
|
5
|
+
import functools
|
|
6
|
+
import itertools
|
|
7
|
+
|
|
8
|
+
from pyramid.exceptions import PredicateMismatch
|
|
9
|
+
from pyramid.httpexceptions import (
|
|
10
|
+
HTTPException,
|
|
11
|
+
HTTPMethodNotAllowed,
|
|
12
|
+
HTTPNotAcceptable,
|
|
13
|
+
HTTPUnsupportedMediaType,
|
|
14
|
+
)
|
|
15
|
+
from pyramid.security import NO_PERMISSION_REQUIRED
|
|
16
|
+
|
|
17
|
+
from kinto.core.cornice.cors import (
|
|
18
|
+
CORS_PARAMETERS,
|
|
19
|
+
apply_cors_post_request,
|
|
20
|
+
get_cors_preflight_view,
|
|
21
|
+
get_cors_validator,
|
|
22
|
+
)
|
|
23
|
+
from kinto.core.cornice.errors import Errors
|
|
24
|
+
from kinto.core.cornice.service import decorate_view
|
|
25
|
+
from kinto.core.cornice.util import (
|
|
26
|
+
content_type_matches,
|
|
27
|
+
current_service,
|
|
28
|
+
is_string,
|
|
29
|
+
match_accept_header,
|
|
30
|
+
match_content_type_header,
|
|
31
|
+
to_list,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_fallback_view(service):
|
|
36
|
+
"""Fallback view for a given service, called when nothing else matches.
|
|
37
|
+
|
|
38
|
+
This method provides the view logic to be executed when the request
|
|
39
|
+
does not match any explicitly-defined view. Its main responsibility
|
|
40
|
+
is to produce an accurate error response, such as HTTPMethodNotAllowed,
|
|
41
|
+
HTTPNotAcceptable or HTTPUnsupportedMediaType.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def _fallback_view(request):
|
|
45
|
+
# Maybe we failed to match any definitions for the request method?
|
|
46
|
+
if request.method not in service.defined_methods:
|
|
47
|
+
response = HTTPMethodNotAllowed()
|
|
48
|
+
response.allow = service.defined_methods
|
|
49
|
+
raise response
|
|
50
|
+
# Maybe we failed to match an acceptable content-type?
|
|
51
|
+
# First search all the definitions to find the acceptable types.
|
|
52
|
+
# XXX: precalculate this like the defined_methods list?
|
|
53
|
+
acceptable = []
|
|
54
|
+
supported_contenttypes = []
|
|
55
|
+
for method, _, args in service.definitions:
|
|
56
|
+
if method != request.method:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if "accept" in args:
|
|
60
|
+
acceptable.extend(service.get_acceptable(method, filter_callables=True))
|
|
61
|
+
acceptable.extend(request.info.get("acceptable", []))
|
|
62
|
+
acceptable = list(set(acceptable))
|
|
63
|
+
|
|
64
|
+
# Now check if that was actually the source of the problem.
|
|
65
|
+
if not request.accept.acceptable_offers(offers=acceptable):
|
|
66
|
+
request.errors.add(
|
|
67
|
+
"header",
|
|
68
|
+
"Accept",
|
|
69
|
+
"Accept header should be one of {0}".format(acceptable).encode("ascii"),
|
|
70
|
+
)
|
|
71
|
+
request.errors.status = HTTPNotAcceptable.code
|
|
72
|
+
error = service.error_handler(request)
|
|
73
|
+
raise error
|
|
74
|
+
|
|
75
|
+
if "content_type" in args:
|
|
76
|
+
supported_contenttypes.extend(
|
|
77
|
+
service.get_contenttypes(method, filter_callables=True)
|
|
78
|
+
)
|
|
79
|
+
supported_contenttypes.extend(request.info.get("supported_contenttypes", []))
|
|
80
|
+
supported_contenttypes = list(set(supported_contenttypes))
|
|
81
|
+
|
|
82
|
+
# Now check if that was actually the source of the problem.
|
|
83
|
+
if not content_type_matches(request, supported_contenttypes):
|
|
84
|
+
request.errors.add(
|
|
85
|
+
"header",
|
|
86
|
+
"Content-Type",
|
|
87
|
+
"Content-Type header should be one of {0}".format(
|
|
88
|
+
supported_contenttypes
|
|
89
|
+
).encode("ascii"),
|
|
90
|
+
)
|
|
91
|
+
request.errors.status = HTTPUnsupportedMediaType.code
|
|
92
|
+
error = service.error_handler(request)
|
|
93
|
+
raise error
|
|
94
|
+
|
|
95
|
+
# In the absence of further information about what went wrong,
|
|
96
|
+
# let upstream deal with the mismatch.
|
|
97
|
+
|
|
98
|
+
# After "custom predicates" feature has been added there is no need in
|
|
99
|
+
# this line. Instead requests will be filtered by "custom predicates"
|
|
100
|
+
# feature filter and exception "404 Not found" error will be raised. In
|
|
101
|
+
# order to avoid unpredictable cases, we left this line in place and
|
|
102
|
+
# excluded it from coverage.
|
|
103
|
+
raise PredicateMismatch(service.name) # pragma: no cover
|
|
104
|
+
|
|
105
|
+
return _fallback_view
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def apply_filters(request, response):
|
|
109
|
+
if request.matched_route is not None:
|
|
110
|
+
# do some sanity checking on the response using filters
|
|
111
|
+
service = current_service(request)
|
|
112
|
+
if service is not None:
|
|
113
|
+
kwargs, ob = getattr(request, "cornice_args", ({}, None))
|
|
114
|
+
for _filter in kwargs.get("filters", []):
|
|
115
|
+
if is_string(_filter) and ob is not None:
|
|
116
|
+
_filter = getattr(ob, _filter)
|
|
117
|
+
try:
|
|
118
|
+
response = _filter(response, request)
|
|
119
|
+
except TypeError:
|
|
120
|
+
response = _filter(response)
|
|
121
|
+
if service.cors_enabled:
|
|
122
|
+
apply_cors_post_request(service, request, response)
|
|
123
|
+
|
|
124
|
+
return response
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def handle_exceptions(exc, request):
|
|
128
|
+
# At this stage, the checks done by the validators had been removed because
|
|
129
|
+
# a new response started (the exception), so we need to do that again.
|
|
130
|
+
if not isinstance(exc, HTTPException):
|
|
131
|
+
raise
|
|
132
|
+
request.info["cors_checked"] = False
|
|
133
|
+
return apply_filters(request, exc)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def add_nosniff_header(request, response):
|
|
137
|
+
"""IE has some rather unfortunately content-type-sniffing behaviour
|
|
138
|
+
that can be used to trigger XSS attacks via a JSON API, as described here:
|
|
139
|
+
|
|
140
|
+
* http://blog.watchfire.com/wfblog/2011/10/json-based-xss-exploitation.html
|
|
141
|
+
* https://superevr.com/blog/2012/exploiting-xss-in-ajax-web-applications/
|
|
142
|
+
|
|
143
|
+
Make cornice safe-by-default against this attack by including the header.
|
|
144
|
+
"""
|
|
145
|
+
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def wrap_request(event):
|
|
149
|
+
"""Adds a "validated" dict, a custom "errors" object and an "info" dict to
|
|
150
|
+
the request object if they don't already exists
|
|
151
|
+
"""
|
|
152
|
+
request = event.request
|
|
153
|
+
request.add_response_callback(apply_filters)
|
|
154
|
+
request.add_response_callback(add_nosniff_header)
|
|
155
|
+
|
|
156
|
+
if not hasattr(request, "validated"):
|
|
157
|
+
setattr(request, "validated", {})
|
|
158
|
+
|
|
159
|
+
if not hasattr(request, "errors"):
|
|
160
|
+
if request.registry.settings.get("available_languages"):
|
|
161
|
+
setattr(request, "errors", Errors(localizer=request.localizer))
|
|
162
|
+
else:
|
|
163
|
+
setattr(request, "errors", Errors())
|
|
164
|
+
|
|
165
|
+
if not hasattr(request, "info"):
|
|
166
|
+
setattr(request, "info", {})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def register_service_views(config, service):
|
|
170
|
+
"""Register the routes of the given service into the pyramid router.
|
|
171
|
+
|
|
172
|
+
:param config: the pyramid configuration object that will be populated.
|
|
173
|
+
:param service: the service object containing the definitions
|
|
174
|
+
"""
|
|
175
|
+
route_name = service.name
|
|
176
|
+
existing_route = service.pyramid_route
|
|
177
|
+
prefix = config.route_prefix or ""
|
|
178
|
+
services = config.registry.cornice_services
|
|
179
|
+
if existing_route:
|
|
180
|
+
route_name = existing_route
|
|
181
|
+
services["__cornice" + existing_route] = service
|
|
182
|
+
else:
|
|
183
|
+
services[prefix + service.path] = service
|
|
184
|
+
|
|
185
|
+
# before doing anything else, register a view for the OPTIONS method
|
|
186
|
+
# if we need to
|
|
187
|
+
if service.cors_enabled and "OPTIONS" not in service.defined_methods:
|
|
188
|
+
service.add_view(
|
|
189
|
+
"options", view=get_cors_preflight_view(service), permission=NO_PERMISSION_REQUIRED
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# register the fallback view, which takes care of returning good error
|
|
193
|
+
# messages to the user-agent
|
|
194
|
+
cors_validator = get_cors_validator(service)
|
|
195
|
+
|
|
196
|
+
# Cornice-specific arguments that pyramid does not know about
|
|
197
|
+
cornice_parameters = (
|
|
198
|
+
"filters",
|
|
199
|
+
"validators",
|
|
200
|
+
"schema",
|
|
201
|
+
"klass",
|
|
202
|
+
"error_handler",
|
|
203
|
+
"deserializer",
|
|
204
|
+
) + CORS_PARAMETERS
|
|
205
|
+
|
|
206
|
+
# 1. register route
|
|
207
|
+
|
|
208
|
+
route_args = {}
|
|
209
|
+
|
|
210
|
+
if hasattr(service, "factory"):
|
|
211
|
+
route_args["factory"] = service.factory
|
|
212
|
+
|
|
213
|
+
routes = config.get_predlist("route")
|
|
214
|
+
for predicate in routes.sorter.names:
|
|
215
|
+
# Do not let the custom predicates handle validation of Header Accept,
|
|
216
|
+
# which will pass it through to pyramid. It is handled by
|
|
217
|
+
# _fallback_view(), because it allows callable.
|
|
218
|
+
if predicate == "accept":
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
if hasattr(service, predicate):
|
|
222
|
+
route_args[predicate] = getattr(service, predicate)
|
|
223
|
+
|
|
224
|
+
# register route when not using exiting pyramid routes
|
|
225
|
+
if not existing_route:
|
|
226
|
+
config.add_route(route_name, service.path, **route_args)
|
|
227
|
+
|
|
228
|
+
# 2. register view(s)
|
|
229
|
+
|
|
230
|
+
for method, view, args in service.definitions:
|
|
231
|
+
args = copy.copy(args) # make a copy of the dict to not modify it
|
|
232
|
+
# Deepcopy only the params we're possibly passing on to pyramid
|
|
233
|
+
# (Some of those in cornice_parameters, e.g. ``schema``, may contain
|
|
234
|
+
# unpickleable values.)
|
|
235
|
+
for item in args:
|
|
236
|
+
if item not in cornice_parameters:
|
|
237
|
+
args[item] = copy.deepcopy(args[item])
|
|
238
|
+
|
|
239
|
+
args["request_method"] = method
|
|
240
|
+
|
|
241
|
+
if service.cors_enabled:
|
|
242
|
+
args["validators"].insert(0, cors_validator)
|
|
243
|
+
|
|
244
|
+
decorated_view = decorate_view(view, dict(args), method, route_args)
|
|
245
|
+
|
|
246
|
+
for item in cornice_parameters:
|
|
247
|
+
if item in args:
|
|
248
|
+
del args[item]
|
|
249
|
+
|
|
250
|
+
# filter predicates defined on Resource
|
|
251
|
+
route_predicates = config.get_predlist("route").sorter.names
|
|
252
|
+
view_predicates = config.get_predlist("view").sorter.names
|
|
253
|
+
for pred in set(route_predicates).difference(view_predicates):
|
|
254
|
+
if pred in args:
|
|
255
|
+
args.pop(pred)
|
|
256
|
+
|
|
257
|
+
# pop and compute predicates which get passed through to Pyramid 1:1
|
|
258
|
+
|
|
259
|
+
predicate_definitions = _pop_complex_predicates(args)
|
|
260
|
+
|
|
261
|
+
if predicate_definitions:
|
|
262
|
+
empty_contenttype = [({"kind": "content_type", "value": ""},)]
|
|
263
|
+
for predicate_list in predicate_definitions + empty_contenttype:
|
|
264
|
+
args = dict(args) # make a copy of the dict to not modify it
|
|
265
|
+
|
|
266
|
+
# prepare view args by evaluating complex predicates
|
|
267
|
+
_mungle_view_args(args, predicate_list)
|
|
268
|
+
|
|
269
|
+
# We register the same view multiple times with different
|
|
270
|
+
# accept / content_type / custom_predicates arguments
|
|
271
|
+
config.add_view(view=decorated_view, route_name=route_name, **args)
|
|
272
|
+
|
|
273
|
+
else:
|
|
274
|
+
# it is a simple view, we don't need to loop on the definitions
|
|
275
|
+
# and just add it one time.
|
|
276
|
+
config.add_view(view=decorated_view, route_name=route_name, **args)
|
|
277
|
+
|
|
278
|
+
if service.definitions:
|
|
279
|
+
# Add the fallback view last
|
|
280
|
+
config.add_view(
|
|
281
|
+
view=get_fallback_view(service),
|
|
282
|
+
route_name=route_name,
|
|
283
|
+
permission=NO_PERMISSION_REQUIRED,
|
|
284
|
+
require_csrf=False,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _pop_complex_predicates(args):
|
|
289
|
+
"""
|
|
290
|
+
Compute the cartesian product of "accept" and "content_type"
|
|
291
|
+
fields to establish all possible predicate combinations.
|
|
292
|
+
|
|
293
|
+
.. seealso::
|
|
294
|
+
|
|
295
|
+
https://github.com/mozilla-services/cornice/pull/91#discussion_r3441384
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
# pop and prepare individual predicate lists
|
|
299
|
+
accept_list = _pop_predicate_definition(args, "accept")
|
|
300
|
+
content_type_list = _pop_predicate_definition(args, "content_type")
|
|
301
|
+
|
|
302
|
+
# compute cartesian product of prepared lists, additionally
|
|
303
|
+
# remove empty elements of input and output lists
|
|
304
|
+
product_input = filter(None, [accept_list, content_type_list])
|
|
305
|
+
|
|
306
|
+
# In Python 3, the filter() function returns an iterator, not a list.
|
|
307
|
+
# http://getpython3.com/diveintopython3/ \
|
|
308
|
+
# porting-code-to-python-3-with-2to3.html#filter
|
|
309
|
+
predicate_product = list(filter(None, itertools.product(*product_input)))
|
|
310
|
+
|
|
311
|
+
return predicate_product
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _pop_predicate_definition(args, kind):
|
|
315
|
+
"""
|
|
316
|
+
Build a dictionary enriched by "kind" of predicate definition list.
|
|
317
|
+
This is required for evaluation in ``_mungle_view_args``.
|
|
318
|
+
"""
|
|
319
|
+
values = to_list(args.pop(kind, ()))
|
|
320
|
+
# In much the same way as filter(), the map() function [in Python 3] now
|
|
321
|
+
# returns an iterator. (In Python 2, it returned a list.)
|
|
322
|
+
# http://getpython3.com/diveintopython3/ \
|
|
323
|
+
# porting-code-to-python-3-with-2to3.html#map
|
|
324
|
+
values = list(map(lambda value: {"kind": kind, "value": value}, values))
|
|
325
|
+
return values
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _mungle_view_args(args, predicate_list):
|
|
329
|
+
"""
|
|
330
|
+
Prepare view args by evaluating complex predicates
|
|
331
|
+
which get passed through to Pyramid 1:1.
|
|
332
|
+
Also resolve predicate definitions passed as callables.
|
|
333
|
+
|
|
334
|
+
.. seealso::
|
|
335
|
+
|
|
336
|
+
https://github.com/mozilla-services/cornice/pull/91#discussion_r3441384
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
# map kind of argument value to function for resolving callables
|
|
340
|
+
callable_map = {
|
|
341
|
+
"accept": match_accept_header,
|
|
342
|
+
"content_type": match_content_type_header,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# iterate and resolve all predicates
|
|
346
|
+
for predicate_entry in predicate_list:
|
|
347
|
+
kind = predicate_entry["kind"]
|
|
348
|
+
value = predicate_entry["value"]
|
|
349
|
+
|
|
350
|
+
# we need to build a custom predicate if argument value is a callable
|
|
351
|
+
predicates = args.get("custom_predicates", [])
|
|
352
|
+
if callable(value):
|
|
353
|
+
func = callable_map[kind]
|
|
354
|
+
predicate_checker = functools.partial(func, value)
|
|
355
|
+
predicates.append(predicate_checker)
|
|
356
|
+
args["custom_predicates"] = predicates
|
|
357
|
+
else:
|
|
358
|
+
# otherwise argument value is just a scalar
|
|
359
|
+
args[kind] = value
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def register_resource_views(config, resource):
|
|
363
|
+
"""Register a resource and it's views.
|
|
364
|
+
|
|
365
|
+
:param config:
|
|
366
|
+
The pyramid configuration object that will be populated.
|
|
367
|
+
:param resource:
|
|
368
|
+
The resource class containing the definitions
|
|
369
|
+
"""
|
|
370
|
+
services = resource._services
|
|
371
|
+
|
|
372
|
+
for service in services.values():
|
|
373
|
+
config.add_cornice_service(service)
|