kinto 18.1.0__py3-none-any.whl → 20.4.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/__init__.py +1 -0
- kinto/__main__.py +1 -19
- kinto/config/kinto.tpl +5 -15
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +21 -8
- 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/errors.py +6 -4
- kinto/core/initialization.py +129 -59
- kinto/core/metrics.py +93 -0
- kinto/core/openapi.py +2 -3
- kinto/core/permission/memory.py +3 -2
- kinto/core/permission/postgresql/__init__.py +9 -9
- kinto/core/permission/testing.py +6 -0
- kinto/core/resource/__init__.py +9 -4
- kinto/core/resource/schema.py +1 -2
- kinto/core/resource/viewset.py +1 -1
- kinto/core/statsd.py +1 -63
- kinto/core/storage/__init__.py +15 -0
- kinto/core/storage/memory.py +20 -3
- kinto/core/storage/postgresql/__init__.py +31 -1
- kinto/core/storage/postgresql/client.py +2 -2
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/pool.py +1 -1
- kinto/core/storage/postgresql/schema.sql +3 -2
- kinto/core/storage/testing.py +41 -1
- kinto/core/testing.py +6 -2
- kinto/core/utils.py +14 -4
- kinto/core/views/batch.py +1 -1
- kinto/core/views/errors.py +4 -3
- kinto/core/views/openapi.py +1 -1
- kinto/plugins/accounts/__init__.py +3 -21
- kinto/plugins/accounts/authentication.py +8 -54
- kinto/plugins/accounts/utils.py +0 -133
- kinto/plugins/accounts/{views/__init__.py → views.py} +7 -62
- kinto/plugins/admin/VERSION +1 -1
- kinto/plugins/admin/build/VERSION +1 -0
- kinto/plugins/admin/build/assets/asn1-EdZsLKOL.js +1 -0
- kinto/plugins/admin/build/assets/clojure-BMjYHr_A.js +1 -0
- kinto/plugins/admin/build/assets/css-BnMrqG3P.js +1 -0
- kinto/plugins/admin/build/assets/index-Cs7JVwIg.css +6 -0
- kinto/plugins/admin/build/assets/index-CylsivYB.js +165 -0
- kinto/plugins/admin/build/assets/javascript-qCveANmP.js +1 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/assets/mllike-CXdrOF99.js +1 -0
- kinto/plugins/admin/build/assets/python-BuPzkPfP.js +1 -0
- kinto/plugins/admin/build/assets/rpm-CTu-6PCP.js +1 -0
- kinto/plugins/admin/build/assets/sql-D0XecflT.js +1 -0
- kinto/plugins/admin/build/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- kinto/plugins/admin/build/index.html +18 -0
- kinto/plugins/default_bucket/__init__.py +1 -2
- kinto/plugins/flush.py +2 -2
- kinto/plugins/history/__init__.py +15 -6
- kinto/plugins/history/listener.py +68 -5
- kinto/plugins/openid/views.py +1 -1
- kinto/plugins/prometheus.py +203 -0
- kinto/plugins/statsd.py +78 -0
- kinto/views/contribute.py +14 -13
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/METADATA +31 -32
- kinto-20.4.0.dist-info/RECORD +149 -0
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/WHEEL +1 -1
- kinto/plugins/accounts/mails.py +0 -96
- kinto/plugins/accounts/views/validation.py +0 -136
- kinto/plugins/quotas/__init__.py +0 -22
- kinto/plugins/quotas/listener.py +0 -226
- kinto/plugins/quotas/scripts.py +0 -80
- kinto/plugins/quotas/utils.py +0 -7
- kinto/scripts.py +0 -41
- kinto-18.1.0.dist-info/RECORD +0 -116
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/entry_points.txt +0 -0
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info/licenses}/LICENSE +0 -0
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/top_level.txt +0 -0
kinto/__init__.py
CHANGED
kinto/__main__.py
CHANGED
|
@@ -9,7 +9,6 @@ from pyramid.paster import bootstrap
|
|
|
9
9
|
from pyramid.scripts import pserve
|
|
10
10
|
|
|
11
11
|
from kinto import __version__
|
|
12
|
-
from kinto import scripts as kinto_scripts
|
|
13
12
|
from kinto.config import init
|
|
14
13
|
from kinto.core import scripts as core_scripts
|
|
15
14
|
from kinto.plugins.accounts import scripts as accounts_scripts
|
|
@@ -33,7 +32,6 @@ def main(args=None):
|
|
|
33
32
|
"migrate",
|
|
34
33
|
"flush-cache",
|
|
35
34
|
"version",
|
|
36
|
-
"rebuild-quotas",
|
|
37
35
|
"create-user",
|
|
38
36
|
)
|
|
39
37
|
subparsers = parser.add_subparsers(
|
|
@@ -107,16 +105,6 @@ def main(args=None):
|
|
|
107
105
|
default=False,
|
|
108
106
|
)
|
|
109
107
|
|
|
110
|
-
elif command == "rebuild-quotas":
|
|
111
|
-
subparser.add_argument(
|
|
112
|
-
"--dry-run",
|
|
113
|
-
action="store_true",
|
|
114
|
-
help="Simulate the rebuild operation and show information",
|
|
115
|
-
dest="dry_run",
|
|
116
|
-
required=False,
|
|
117
|
-
default=False,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
108
|
elif command == "start":
|
|
121
109
|
subparser.add_argument(
|
|
122
110
|
"--reload",
|
|
@@ -161,8 +149,7 @@ def main(args=None):
|
|
|
161
149
|
if not backend:
|
|
162
150
|
while True:
|
|
163
151
|
prompt = (
|
|
164
|
-
"Select the backend you would like to use: "
|
|
165
|
-
"(1 - postgresql, default - memory) "
|
|
152
|
+
"Select the backend you would like to use: (1 - postgresql, default - memory) "
|
|
166
153
|
)
|
|
167
154
|
answer = input(prompt).strip()
|
|
168
155
|
try:
|
|
@@ -215,11 +202,6 @@ def main(args=None):
|
|
|
215
202
|
env = bootstrap(config_file, options={"command": "flush-cache"})
|
|
216
203
|
core_scripts.flush_cache(env)
|
|
217
204
|
|
|
218
|
-
elif which_command == "rebuild-quotas":
|
|
219
|
-
dry_run = parsed_args["dry_run"]
|
|
220
|
-
env = bootstrap(config_file, options={"command": "rebuild-quotas"})
|
|
221
|
-
return kinto_scripts.rebuild_quotas(env, dry_run=dry_run)
|
|
222
|
-
|
|
223
205
|
elif which_command == "create-user":
|
|
224
206
|
username = parsed_args["username"]
|
|
225
207
|
password = parsed_args["password"]
|
kinto/config/kinto.tpl
CHANGED
|
@@ -36,7 +36,6 @@ kinto.includes = kinto.plugins.default_bucket
|
|
|
36
36
|
kinto.plugins.admin
|
|
37
37
|
kinto.plugins.accounts
|
|
38
38
|
# kinto.plugins.history
|
|
39
|
-
# kinto.plugins.quotas
|
|
40
39
|
|
|
41
40
|
# Backends
|
|
42
41
|
# https://kinto.readthedocs.io/en/latest/configuration/settings.html#storage
|
|
@@ -104,18 +103,6 @@ kinto.account_create_principals = system.Everyone
|
|
|
104
103
|
kinto.account_write_principals = account:admin
|
|
105
104
|
# Allow administrators to create buckets
|
|
106
105
|
kinto.bucket_create_principals = account:admin
|
|
107
|
-
# Enable the "account_validation" option.
|
|
108
|
-
# kinto.account_validation = true
|
|
109
|
-
# Set the sender for the validation email.
|
|
110
|
-
# kinto.account_validation.email_sender = "admin@example.com"
|
|
111
|
-
# Set the regular expression used to validate a proper email address.
|
|
112
|
-
# kinto.account_validation.email_regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"
|
|
113
|
-
|
|
114
|
-
# Mail configuration (needed for the account validation option), see https://docs.pylonsproject.org/projects/pyramid_mailer/en/latest/#configuration
|
|
115
|
-
# mail.host = localhost
|
|
116
|
-
# mail.port = 25
|
|
117
|
-
# mail.username = someusername
|
|
118
|
-
# mail.password = somepassword
|
|
119
106
|
|
|
120
107
|
# Notifications
|
|
121
108
|
# https://kinto.readthedocs.io/en/latest/configuration/settings.html#notifications
|
|
@@ -258,7 +245,7 @@ keys = root, kinto
|
|
|
258
245
|
keys = console
|
|
259
246
|
|
|
260
247
|
[formatters]
|
|
261
|
-
keys = color
|
|
248
|
+
keys = color, json
|
|
262
249
|
|
|
263
250
|
[logger_root]
|
|
264
251
|
level = INFO
|
|
@@ -271,10 +258,13 @@ qualname = kinto
|
|
|
271
258
|
propagate = 0
|
|
272
259
|
|
|
273
260
|
[handler_console]
|
|
274
|
-
class =
|
|
261
|
+
class = kinto.core.StreamHandlerWithRequestID
|
|
275
262
|
args = (sys.stderr,)
|
|
276
263
|
level = NOTSET
|
|
277
264
|
formatter = color
|
|
278
265
|
|
|
279
266
|
[formatter_color]
|
|
280
267
|
class = logging_color_formatter.ColorFormatter
|
|
268
|
+
|
|
269
|
+
[formatter_json]
|
|
270
|
+
class = kinto.core.JsonLogFormatter
|
kinto/contribute.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Kinto",
|
|
3
|
+
"description": "Kinto is a minimalist JSON storage service with synchronisation and sharing abilities.",
|
|
4
|
+
"repository": {
|
|
5
|
+
"url": "https://github.com/Kinto/kinto",
|
|
6
|
+
"license": "Apache License (2.0)",
|
|
7
|
+
"tests": "https://github.com/Kinto/kinto/actions"
|
|
8
|
+
},
|
|
9
|
+
"participate": {
|
|
10
|
+
"home": "https://kinto.readthedocs.io/en/latest/community.html#how-to-contribute",
|
|
11
|
+
"docs": "https://kinto.readthedocs.io/",
|
|
12
|
+
"irc": "irc://irc.freenode.org/#kinto",
|
|
13
|
+
"irc-contacts": ["alexis", "leplatrem", "magopian", "natim", "NiKo`"]
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"list": "https://github.com/Kinto/kinto/issues",
|
|
17
|
+
"report": "https://github.com/Kinto/kinto/issues/new"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"JSON",
|
|
21
|
+
"Python",
|
|
22
|
+
"Pyramid",
|
|
23
|
+
"REST",
|
|
24
|
+
"API",
|
|
25
|
+
"Database"
|
|
26
|
+
]
|
|
27
|
+
}
|
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,
|
|
@@ -66,8 +66,8 @@ DEFAULT_SETTINGS = {
|
|
|
66
66
|
"kinto.core.initialization.setup_authentication",
|
|
67
67
|
"kinto.core.initialization.setup_backoff",
|
|
68
68
|
"kinto.core.initialization.setup_sentry",
|
|
69
|
-
"kinto.core.initialization.setup_statsd",
|
|
70
69
|
"kinto.core.initialization.setup_listeners",
|
|
70
|
+
"kinto.core.initialization.setup_metrics",
|
|
71
71
|
"kinto.core.events.setup_transaction_hook",
|
|
72
72
|
),
|
|
73
73
|
"event_listeners": "",
|
|
@@ -96,6 +96,7 @@ DEFAULT_SETTINGS = {
|
|
|
96
96
|
"statsd_backend": "kinto.core.statsd",
|
|
97
97
|
"statsd_prefix": "kinto.core",
|
|
98
98
|
"statsd_url": None,
|
|
99
|
+
"metrics_matchdict_fields": [],
|
|
99
100
|
"storage_backend": "",
|
|
100
101
|
"storage_url": "",
|
|
101
102
|
"storage_max_fetch_size": 10000,
|
|
@@ -109,10 +110,8 @@ DEFAULT_SETTINGS = {
|
|
|
109
110
|
"trailing_slash_redirect_ttl_seconds": 3600,
|
|
110
111
|
"multiauth.groupfinder": "kinto.core.authorization.groupfinder",
|
|
111
112
|
"multiauth.policies": "",
|
|
112
|
-
"multiauth.policy.basicauth.use":
|
|
113
|
-
|
|
114
|
-
),
|
|
115
|
-
"multiauth.authorization_policy": ("kinto.core.authorization." "AuthorizationPolicy"),
|
|
113
|
+
"multiauth.policy.basicauth.use": "kinto.core.authentication.BasicAuthAuthenticationPolicy",
|
|
114
|
+
"multiauth.authorization_policy": "kinto.core.authorization.AuthorizationPolicy",
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
|
|
@@ -152,6 +151,20 @@ class JsonLogFormatter(dockerflow_logging.JsonLogFormatter):
|
|
|
152
151
|
self.logger_name = logger_name
|
|
153
152
|
|
|
154
153
|
|
|
154
|
+
class StreamHandlerWithRequestID(logging.StreamHandler):
|
|
155
|
+
"""
|
|
156
|
+
A custom StreamHandler that adds the Dockerflow's `RequestIdLogFilter`.
|
|
157
|
+
|
|
158
|
+
Defining a custom handler seems to be the only way to bypass the fact that
|
|
159
|
+
``logging.config.fileConfig()`` does not load filters from ``.ini`` files.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, *args, **kwargs):
|
|
163
|
+
super().__init__(*args, **kwargs)
|
|
164
|
+
filter_ = dockerflow_logging.RequestIdLogFilter()
|
|
165
|
+
self.addFilter(filter_)
|
|
166
|
+
|
|
167
|
+
|
|
155
168
|
def get_user_info(request):
|
|
156
169
|
# Default user info (shown in hello view for example).
|
|
157
170
|
user_info = {"id": request.prefixed_userid, "principals": request.prefixed_principals}
|
|
@@ -187,10 +200,10 @@ def includeme(config):
|
|
|
187
200
|
config.add_request_method(events.notify_resource_event, name="notify_resource_event")
|
|
188
201
|
|
|
189
202
|
# Setup cornice.
|
|
190
|
-
config.include("cornice")
|
|
203
|
+
config.include("kinto.core.cornice")
|
|
191
204
|
|
|
192
205
|
# Setup cornice api documentation
|
|
193
|
-
config.include("cornice_swagger")
|
|
206
|
+
config.include("kinto.core.cornice_swagger")
|
|
194
207
|
|
|
195
208
|
# Per-request transaction.
|
|
196
209
|
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
|