kinto 23.2.1__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.
- kinto/__init__.py +92 -0
- kinto/__main__.py +249 -0
- kinto/authorization.py +134 -0
- kinto/config/__init__.py +94 -0
- kinto/config/kinto.tpl +270 -0
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +246 -0
- kinto/core/authentication.py +48 -0
- kinto/core/authorization.py +311 -0
- kinto/core/cache/__init__.py +131 -0
- kinto/core/cache/memcached.py +112 -0
- kinto/core/cache/memory.py +104 -0
- kinto/core/cache/postgresql/__init__.py +178 -0
- kinto/core/cache/postgresql/schema.sql +23 -0
- kinto/core/cache/testing.py +208 -0
- 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/decorators.py +74 -0
- kinto/core/errors.py +216 -0
- kinto/core/events.py +301 -0
- kinto/core/initialization.py +738 -0
- kinto/core/listeners/__init__.py +9 -0
- kinto/core/metrics.py +94 -0
- kinto/core/openapi.py +115 -0
- kinto/core/permission/__init__.py +202 -0
- kinto/core/permission/memory.py +167 -0
- kinto/core/permission/postgresql/__init__.py +489 -0
- kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
- kinto/core/permission/postgresql/schema.sql +41 -0
- kinto/core/permission/testing.py +487 -0
- kinto/core/resource/__init__.py +1311 -0
- kinto/core/resource/model.py +412 -0
- kinto/core/resource/schema.py +502 -0
- kinto/core/resource/viewset.py +230 -0
- kinto/core/schema.py +119 -0
- kinto/core/scripts.py +50 -0
- kinto/core/statsd.py +1 -0
- kinto/core/storage/__init__.py +436 -0
- kinto/core/storage/exceptions.py +53 -0
- kinto/core/storage/generators.py +58 -0
- kinto/core/storage/memory.py +651 -0
- kinto/core/storage/postgresql/__init__.py +1131 -0
- kinto/core/storage/postgresql/client.py +120 -0
- kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
- kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
- kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
- kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
- kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
- kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
- kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
- kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
- kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
- kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
- kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
- kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
- kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
- kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
- kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
- kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
- kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
- kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
- kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
- kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
- kinto/core/storage/postgresql/migrator.py +98 -0
- kinto/core/storage/postgresql/pool.py +55 -0
- kinto/core/storage/postgresql/schema.sql +143 -0
- kinto/core/storage/testing.py +1857 -0
- kinto/core/storage/utils.py +37 -0
- kinto/core/testing.py +182 -0
- kinto/core/utils.py +553 -0
- kinto/core/views/__init__.py +0 -0
- kinto/core/views/batch.py +163 -0
- kinto/core/views/errors.py +145 -0
- kinto/core/views/heartbeat.py +106 -0
- kinto/core/views/hello.py +69 -0
- kinto/core/views/openapi.py +35 -0
- kinto/core/views/version.py +50 -0
- kinto/events.py +3 -0
- kinto/plugins/__init__.py +0 -0
- kinto/plugins/accounts/__init__.py +94 -0
- kinto/plugins/accounts/authentication.py +63 -0
- kinto/plugins/accounts/scripts.py +61 -0
- kinto/plugins/accounts/utils.py +13 -0
- kinto/plugins/accounts/views.py +136 -0
- kinto/plugins/admin/README.md +3 -0
- kinto/plugins/admin/VERSION +1 -0
- kinto/plugins/admin/__init__.py +40 -0
- kinto/plugins/admin/build/VERSION +1 -0
- kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
- kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/index.html +18 -0
- kinto/plugins/admin/public/help.html +25 -0
- kinto/plugins/admin/views.py +42 -0
- kinto/plugins/default_bucket/__init__.py +191 -0
- kinto/plugins/flush.py +28 -0
- kinto/plugins/history/__init__.py +65 -0
- kinto/plugins/history/listener.py +181 -0
- kinto/plugins/history/views.py +66 -0
- kinto/plugins/openid/__init__.py +131 -0
- kinto/plugins/openid/utils.py +14 -0
- kinto/plugins/openid/views.py +193 -0
- kinto/plugins/prometheus.py +300 -0
- kinto/plugins/statsd.py +85 -0
- kinto/schema_validation.py +135 -0
- kinto/views/__init__.py +34 -0
- kinto/views/admin.py +195 -0
- kinto/views/buckets.py +45 -0
- kinto/views/collections.py +58 -0
- kinto/views/contribute.py +39 -0
- kinto/views/groups.py +90 -0
- kinto/views/permissions.py +235 -0
- kinto/views/records.py +133 -0
- kinto-23.2.1.dist-info/METADATA +232 -0
- kinto-23.2.1.dist-info/RECORD +142 -0
- kinto-23.2.1.dist-info/WHEEL +5 -0
- kinto-23.2.1.dist-info/entry_points.txt +5 -0
- kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
- kinto-23.2.1.dist-info/top_level.txt +1 -0
kinto/core/utils.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import collections.abc as collections_abc
|
|
2
|
+
import functools
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from base64 import b64decode, b64encode
|
|
9
|
+
from binascii import hexlify
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from urllib.parse import unquote
|
|
12
|
+
|
|
13
|
+
import jsonpatch
|
|
14
|
+
import rapidjson
|
|
15
|
+
from colander import null
|
|
16
|
+
from pyramid import httpexceptions
|
|
17
|
+
from pyramid.authorization import Authenticated
|
|
18
|
+
from pyramid.interfaces import IRoutesMapper
|
|
19
|
+
from pyramid.request import Request, apply_request_extensions
|
|
20
|
+
from pyramid.settings import aslist
|
|
21
|
+
from pyramid.view import render_view_to_response
|
|
22
|
+
|
|
23
|
+
from kinto.core.cornice import cors
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import sqlalchemy
|
|
28
|
+
except ImportError: # pragma: no cover
|
|
29
|
+
sqlalchemy = None
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import memcache
|
|
33
|
+
except ImportError: # pragma: no cover
|
|
34
|
+
memcache = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class json:
|
|
38
|
+
def dumps(v, **kw):
|
|
39
|
+
kw.setdefault("bytes_mode", rapidjson.BM_NONE)
|
|
40
|
+
return rapidjson.dumps(v, **kw)
|
|
41
|
+
|
|
42
|
+
def load(v, **kw):
|
|
43
|
+
kw.setdefault("number_mode", rapidjson.NM_NATIVE)
|
|
44
|
+
return rapidjson.load(v, **kw)
|
|
45
|
+
|
|
46
|
+
def loads(v, **kw):
|
|
47
|
+
kw.setdefault("number_mode", rapidjson.NM_NATIVE)
|
|
48
|
+
return rapidjson.loads(v, **kw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
json_serializer = json.dumps
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def strip_whitespace(v):
|
|
55
|
+
"""Remove whitespace, newlines, and tabs from the beginning/end
|
|
56
|
+
of a string.
|
|
57
|
+
|
|
58
|
+
:param str v: the string to strip.
|
|
59
|
+
:rtype: str
|
|
60
|
+
"""
|
|
61
|
+
return v.strip(" \t\n\r") if v is not null else v
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def msec_time():
|
|
65
|
+
"""Return current epoch time in milliseconds.
|
|
66
|
+
|
|
67
|
+
:rtype: int
|
|
68
|
+
"""
|
|
69
|
+
return int(time.time() * 1000.0) # floor
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def classname(obj):
|
|
73
|
+
"""Get a classname from an object.
|
|
74
|
+
|
|
75
|
+
:rtype: str
|
|
76
|
+
"""
|
|
77
|
+
return obj.__class__.__name__.lower()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def merge_dicts(a, b):
|
|
81
|
+
"""Merge b into a recursively, without overwriting values.
|
|
82
|
+
|
|
83
|
+
:param dict a: the dict that will be altered with values of `b`.
|
|
84
|
+
"""
|
|
85
|
+
for k, v in b.items():
|
|
86
|
+
if isinstance(v, dict):
|
|
87
|
+
merge_dicts(a.setdefault(k, {}), v)
|
|
88
|
+
else:
|
|
89
|
+
a.setdefault(k, v)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def recursive_update_dict(root, changes, ignores=()):
|
|
93
|
+
"""Update recursively all the entries from a dict and it's children dicts.
|
|
94
|
+
|
|
95
|
+
:param dict root: root dictionary
|
|
96
|
+
:param dict changes: dictonary where changes should be made (default=root)
|
|
97
|
+
:returns dict newd: dictionary with removed entries of val.
|
|
98
|
+
"""
|
|
99
|
+
if isinstance(changes, dict):
|
|
100
|
+
for k, v in changes.items():
|
|
101
|
+
if isinstance(v, dict):
|
|
102
|
+
if k not in root:
|
|
103
|
+
root[k] = {}
|
|
104
|
+
recursive_update_dict(root[k], v, ignores)
|
|
105
|
+
elif v in ignores:
|
|
106
|
+
if k in root:
|
|
107
|
+
root.pop(k)
|
|
108
|
+
else:
|
|
109
|
+
root[k] = v
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def random_bytes_hex(bytes_length):
|
|
113
|
+
"""Return a hexstring of bytes_length cryptographic-friendly random bytes.
|
|
114
|
+
|
|
115
|
+
:param int bytes_length: number of random bytes.
|
|
116
|
+
:rtype: str
|
|
117
|
+
"""
|
|
118
|
+
return hexlify(os.urandom(bytes_length)).decode("utf-8")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def native_value(value):
|
|
122
|
+
"""Convert string value to native python values.
|
|
123
|
+
|
|
124
|
+
:param str value: value to interprete.
|
|
125
|
+
:returns: the value coerced to python type
|
|
126
|
+
"""
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
try:
|
|
129
|
+
value = json.loads(value)
|
|
130
|
+
except ValueError:
|
|
131
|
+
return value
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def read_env(key, value):
|
|
136
|
+
"""Read the setting key from environment variables.
|
|
137
|
+
|
|
138
|
+
:param key: the setting name
|
|
139
|
+
:param value: default value if undefined in environment
|
|
140
|
+
:returns: the value from environment, coerced to python type, or the (uncoerced) default value
|
|
141
|
+
"""
|
|
142
|
+
envkey = key.replace(".", "_").replace("-", "_").upper()
|
|
143
|
+
if envkey in os.environ:
|
|
144
|
+
return native_value(os.environ[envkey])
|
|
145
|
+
return value
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def encode64(content, encoding="utf-8"):
|
|
149
|
+
"""Encode some content in base64.
|
|
150
|
+
|
|
151
|
+
:rtype: str
|
|
152
|
+
"""
|
|
153
|
+
return b64encode(content.encode(encoding)).decode(encoding)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def decode64(encoded_content, encoding="utf-8"):
|
|
157
|
+
"""Decode some base64 encoded content.
|
|
158
|
+
|
|
159
|
+
:rtype: str
|
|
160
|
+
"""
|
|
161
|
+
return b64decode(encoded_content.encode(encoding)).decode(encoding)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def hmac_digest(secret, message, encoding="utf-8"):
|
|
165
|
+
"""Return hex digest of a message HMAC using secret"""
|
|
166
|
+
if isinstance(secret, str):
|
|
167
|
+
secret = secret.encode(encoding)
|
|
168
|
+
return hmac.new(secret, message.encode(encoding), hashlib.sha256).hexdigest()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def dict_subset(d, keys):
|
|
172
|
+
"""Return a dict with the specified keys"""
|
|
173
|
+
result = {}
|
|
174
|
+
|
|
175
|
+
for key in keys:
|
|
176
|
+
if "." in key:
|
|
177
|
+
field, subfield = key.split(".", 1)
|
|
178
|
+
if isinstance(d.get(field), collections_abc.Mapping):
|
|
179
|
+
subvalue = dict_subset(d[field], [subfield])
|
|
180
|
+
result[field] = dict_merge(subvalue, result.get(field, {}))
|
|
181
|
+
elif field in d:
|
|
182
|
+
result[field] = d[field]
|
|
183
|
+
else:
|
|
184
|
+
if key in d:
|
|
185
|
+
result[key] = d[key]
|
|
186
|
+
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def dict_merge(a, b):
|
|
191
|
+
"""Merge the two specified dicts"""
|
|
192
|
+
result = dict(**b)
|
|
193
|
+
for key, value in a.items():
|
|
194
|
+
if isinstance(value, collections_abc.Mapping):
|
|
195
|
+
value = dict_merge(value, result.setdefault(key, {}))
|
|
196
|
+
result[key] = value
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def find_nested_value(d, path, default=None):
|
|
201
|
+
"""Finds a nested value in a dict from a dotted path key string.
|
|
202
|
+
|
|
203
|
+
:param dict d: the dict to retrieve nested value from
|
|
204
|
+
:param str path: the path to the nested value, in dot notation
|
|
205
|
+
:returns: the nested value if any was found, or None
|
|
206
|
+
"""
|
|
207
|
+
if path in d:
|
|
208
|
+
return d.get(path)
|
|
209
|
+
|
|
210
|
+
# the challenge is to identify what is the root key, as dict keys may
|
|
211
|
+
# contain dot characters themselves
|
|
212
|
+
parts = path.split(".")
|
|
213
|
+
|
|
214
|
+
# build a list of all possible root keys from all the path parts
|
|
215
|
+
candidates = [".".join(parts[: i + 1]) for i in range(len(parts))]
|
|
216
|
+
|
|
217
|
+
# we start with the longest candidate paths as they're most likely to be the
|
|
218
|
+
# ones we want if they match
|
|
219
|
+
root = next((key for key in reversed(candidates) if key in d), None)
|
|
220
|
+
|
|
221
|
+
# if no valid root candidates were found, the path is invalid; abandon
|
|
222
|
+
if root is None or not isinstance(d.get(root), dict):
|
|
223
|
+
return default
|
|
224
|
+
|
|
225
|
+
# we have our root key, extract the new subpath and recur
|
|
226
|
+
subpath = path.replace(root + ".", "", 1)
|
|
227
|
+
return find_nested_value(d.get(root), subpath, default=default)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class COMPARISON(Enum):
|
|
231
|
+
LT = "<"
|
|
232
|
+
MIN = ">="
|
|
233
|
+
MAX = "<="
|
|
234
|
+
NOT = "!="
|
|
235
|
+
EQ = "=="
|
|
236
|
+
GT = ">"
|
|
237
|
+
IN = "in"
|
|
238
|
+
EXCLUDE = "exclude"
|
|
239
|
+
LIKE = "like"
|
|
240
|
+
HAS = "has"
|
|
241
|
+
# The order matters here because we want to match
|
|
242
|
+
# contains_any before contains_
|
|
243
|
+
CONTAINS_ANY = "contains_any"
|
|
244
|
+
CONTAINS = "contains"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def reapply_cors(request, response):
|
|
248
|
+
"""Reapply cors headers to the new response with regards to the request.
|
|
249
|
+
|
|
250
|
+
We need to re-apply the CORS checks done by Cornice, in case we're
|
|
251
|
+
recreating the response from scratch.
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
service = request.current_service
|
|
255
|
+
if service:
|
|
256
|
+
request.info["cors_checked"] = False
|
|
257
|
+
cors.apply_cors_post_request(service, request, response)
|
|
258
|
+
response = cors.ensure_origin(service, request, response)
|
|
259
|
+
else:
|
|
260
|
+
# No existing service is concerned, and Cornice is not implied.
|
|
261
|
+
origin = request.headers.get("Origin")
|
|
262
|
+
if origin:
|
|
263
|
+
settings = request.registry.settings
|
|
264
|
+
allowed_origins = set(aslist(settings["cors_origins"]))
|
|
265
|
+
required_origins = {"*", origin}
|
|
266
|
+
matches = allowed_origins.intersection(required_origins)
|
|
267
|
+
if matches:
|
|
268
|
+
response.headers["Access-Control-Allow-Origin"] = matches.pop()
|
|
269
|
+
|
|
270
|
+
# Import service here because kinto.core import utils
|
|
271
|
+
from kinto.core import Service
|
|
272
|
+
|
|
273
|
+
if Service.default_cors_headers: # pragma: no branch
|
|
274
|
+
headers = ",".join(Service.default_cors_headers)
|
|
275
|
+
response.headers["Access-Control-Expose-Headers"] = headers
|
|
276
|
+
return response
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def log_context(request, **kwargs):
|
|
280
|
+
"""Bind information to the current request summary log."""
|
|
281
|
+
non_empty = {k: v for k, v in kwargs.items() if v is not None}
|
|
282
|
+
try:
|
|
283
|
+
request._log_context.update(**non_empty)
|
|
284
|
+
except AttributeError:
|
|
285
|
+
request._log_context = non_empty
|
|
286
|
+
return request._log_context
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def current_service(request):
|
|
290
|
+
"""Return the Cornice service matching the specified request.
|
|
291
|
+
|
|
292
|
+
:returns: the service or None if unmatched.
|
|
293
|
+
:rtype: kinto.core.cornice.Service
|
|
294
|
+
"""
|
|
295
|
+
if request.matched_route:
|
|
296
|
+
services = request.registry.cornice_services
|
|
297
|
+
pattern = request.matched_route.pattern
|
|
298
|
+
try:
|
|
299
|
+
service = services[pattern]
|
|
300
|
+
except KeyError:
|
|
301
|
+
return None
|
|
302
|
+
else:
|
|
303
|
+
return service
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def current_resource_name(request):
|
|
307
|
+
"""Return the name used when the kinto.core resource was registered along its
|
|
308
|
+
viewset.
|
|
309
|
+
|
|
310
|
+
:returns: the resource identifier.
|
|
311
|
+
:rtype: str
|
|
312
|
+
"""
|
|
313
|
+
service = current_service(request)
|
|
314
|
+
resource_name = service.viewset.get_name(service.resource)
|
|
315
|
+
return resource_name
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def prefixed_userid(request):
|
|
319
|
+
"""In Kinto users ids are prefixed with the policy name that is
|
|
320
|
+
contained in Pyramid Multiauth.
|
|
321
|
+
If a custom authn policy is used, without authn_type, this method returns
|
|
322
|
+
the user id without prefix.
|
|
323
|
+
"""
|
|
324
|
+
# If pyramid_multiauth is used, a ``authn_type`` is set on request
|
|
325
|
+
# when a policy succesfully authenticates a user.
|
|
326
|
+
# (see :func:`kinto.core.initialization.setup_authentication`)
|
|
327
|
+
authn_type = getattr(request, "authn_type", None)
|
|
328
|
+
if authn_type is not None:
|
|
329
|
+
return f"{authn_type}:{request.selected_userid}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def prefixed_principals(request):
|
|
333
|
+
"""
|
|
334
|
+
:returns: the list principals with prefixed user id.
|
|
335
|
+
"""
|
|
336
|
+
principals = request.effective_principals
|
|
337
|
+
if Authenticated not in principals:
|
|
338
|
+
return principals
|
|
339
|
+
|
|
340
|
+
# Remove unprefixed user id on effective_principals to avoid conflicts.
|
|
341
|
+
# (it is added via Pyramid Authn policy effective principals)
|
|
342
|
+
prefix, userid = request.prefixed_userid.split(":", 1)
|
|
343
|
+
principals = [p for p in principals if p != userid]
|
|
344
|
+
|
|
345
|
+
if request.prefixed_userid not in principals:
|
|
346
|
+
principals = [request.prefixed_userid] + principals
|
|
347
|
+
|
|
348
|
+
return principals
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_request(original, dict_obj):
|
|
352
|
+
"""
|
|
353
|
+
Transform a dict object into a :class:`pyramid.request.Request` object.
|
|
354
|
+
|
|
355
|
+
It sets a ``parent`` attribute on the resulting request assigned with
|
|
356
|
+
the `original` request specified.
|
|
357
|
+
|
|
358
|
+
:param original: the original request.
|
|
359
|
+
:param dict_obj: a dict object with the sub-request specifications.
|
|
360
|
+
"""
|
|
361
|
+
api_prefix = "/{}".format(original.upath_info.split("/")[1])
|
|
362
|
+
path = dict_obj["path"]
|
|
363
|
+
if not path.startswith(api_prefix):
|
|
364
|
+
path = api_prefix + path
|
|
365
|
+
|
|
366
|
+
path = path.encode("utf-8")
|
|
367
|
+
|
|
368
|
+
method = dict_obj.get("method") or "GET"
|
|
369
|
+
|
|
370
|
+
headers = dict(original.headers)
|
|
371
|
+
headers.update(**dict_obj.get("headers") or {})
|
|
372
|
+
# Body can have different length, do not use original header.
|
|
373
|
+
headers.pop("Content-Length", None)
|
|
374
|
+
|
|
375
|
+
payload = dict_obj.get("body") or ""
|
|
376
|
+
|
|
377
|
+
# Payload is always a dict (from ``BatchRequestSchema.body``).
|
|
378
|
+
# Send it as JSON for subrequests.
|
|
379
|
+
if isinstance(payload, dict):
|
|
380
|
+
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
381
|
+
payload = json.dumps(payload)
|
|
382
|
+
|
|
383
|
+
request = Request.blank(
|
|
384
|
+
path=path.decode("latin-1"), headers=headers, POST=payload, method=method
|
|
385
|
+
)
|
|
386
|
+
request.registry = original.registry
|
|
387
|
+
apply_request_extensions(request)
|
|
388
|
+
|
|
389
|
+
# This is used to distinguish subrequests from direct incoming requests.
|
|
390
|
+
# See :func:`kinto.core.initialization.setup_logging()`
|
|
391
|
+
request.parent = original
|
|
392
|
+
|
|
393
|
+
return request
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def build_response(response, request):
|
|
397
|
+
"""
|
|
398
|
+
Transform a :class:`pyramid.response.Response` object into a serializable
|
|
399
|
+
dict.
|
|
400
|
+
|
|
401
|
+
:param response: a response object, returned by Pyramid.
|
|
402
|
+
:param request: the request that was used to get the response.
|
|
403
|
+
"""
|
|
404
|
+
dict_obj = {}
|
|
405
|
+
dict_obj["path"] = unquote(request.path)
|
|
406
|
+
dict_obj["status"] = response.status_code
|
|
407
|
+
dict_obj["headers"] = dict(response.headers)
|
|
408
|
+
|
|
409
|
+
body = ""
|
|
410
|
+
if request.method != "HEAD":
|
|
411
|
+
# XXX : Pyramid should not have built response body for HEAD!
|
|
412
|
+
try:
|
|
413
|
+
body = response.json
|
|
414
|
+
except ValueError:
|
|
415
|
+
body = response.body
|
|
416
|
+
dict_obj["body"] = body
|
|
417
|
+
|
|
418
|
+
return dict_obj
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def follow_subrequest(request, subrequest, **kwargs):
|
|
422
|
+
"""Run a subrequest (e.g. batch), and follow the redirection if any.
|
|
423
|
+
|
|
424
|
+
:rtype: tuple
|
|
425
|
+
:returns: the response and the redirection request (or `subrequest`
|
|
426
|
+
if no redirection happened.)
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
try:
|
|
430
|
+
return request.invoke_subrequest(subrequest, **kwargs), subrequest
|
|
431
|
+
except Exception as e:
|
|
432
|
+
resp = render_view_to_response(e, subrequest)
|
|
433
|
+
if not resp or resp.status_code >= 500:
|
|
434
|
+
raise e
|
|
435
|
+
raise resp
|
|
436
|
+
except httpexceptions.HTTPRedirection as e:
|
|
437
|
+
new_location = e.headers["Location"]
|
|
438
|
+
new_request = Request.blank(
|
|
439
|
+
path=new_location,
|
|
440
|
+
headers=subrequest.headers,
|
|
441
|
+
POST=subrequest.body,
|
|
442
|
+
method=subrequest.method,
|
|
443
|
+
)
|
|
444
|
+
new_request.bound_data = subrequest.bound_data
|
|
445
|
+
new_request.parent = getattr(subrequest, "parent", None)
|
|
446
|
+
return request.invoke_subrequest(new_request, **kwargs), new_request
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def strip_uri_prefix(path):
|
|
450
|
+
"""
|
|
451
|
+
Remove potential version prefix in URI.
|
|
452
|
+
"""
|
|
453
|
+
return re.sub(r"^(/v\d+)?", "", str(path))
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def view_lookup(request, uri):
|
|
457
|
+
"""
|
|
458
|
+
A convenience method for view_lookup_registry when you have a request.
|
|
459
|
+
|
|
460
|
+
:param request: the current request (used to obtain registry).
|
|
461
|
+
:param uri: a plural or object endpoint URI.
|
|
462
|
+
:rtype: tuple
|
|
463
|
+
:returns: the resource name and the associated matchdict.
|
|
464
|
+
"""
|
|
465
|
+
return view_lookup_registry(request.registry, uri)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def view_lookup_registry(registry, uri):
|
|
469
|
+
"""
|
|
470
|
+
Look-up the specified `uri` and return the associated resource name
|
|
471
|
+
along the match dict.
|
|
472
|
+
|
|
473
|
+
:param registry: the application's registry.
|
|
474
|
+
:param uri: a plural or object endpoint URI.
|
|
475
|
+
:rtype: tuple
|
|
476
|
+
:returns: the resource name and the associated matchdict.
|
|
477
|
+
"""
|
|
478
|
+
api_prefix = f"/{registry.route_prefix}"
|
|
479
|
+
path = api_prefix + uri
|
|
480
|
+
|
|
481
|
+
q = registry.queryUtility
|
|
482
|
+
routes_mapper = q(IRoutesMapper)
|
|
483
|
+
|
|
484
|
+
fakerequest = Request.blank(path=path)
|
|
485
|
+
info = routes_mapper(fakerequest)
|
|
486
|
+
matchdict, route = info["match"], info["route"]
|
|
487
|
+
if route is None:
|
|
488
|
+
raise ValueError("URI has no route")
|
|
489
|
+
|
|
490
|
+
resource_name = route.name.replace("-object", "").replace("-plural", "")
|
|
491
|
+
return resource_name, matchdict
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def instance_uri(request, resource_name, **params):
|
|
495
|
+
"""Return the URI for the given resource."""
|
|
496
|
+
return strip_uri_prefix(request.route_path(f"{resource_name}-object", **params))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def instance_uri_registry(registry, resource_name, **params):
|
|
500
|
+
"""Return the URI for the given resource, even if you don't have a request.
|
|
501
|
+
|
|
502
|
+
This gins up a request using Request.blank and so does not support
|
|
503
|
+
any routes with pregenerators.
|
|
504
|
+
"""
|
|
505
|
+
request = Request.blank(path="")
|
|
506
|
+
request.registry = registry
|
|
507
|
+
return instance_uri(request, resource_name, **params)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def apply_json_patch(obj, ops):
|
|
511
|
+
"""
|
|
512
|
+
Apply JSON Patch operations using jsonpatch.
|
|
513
|
+
|
|
514
|
+
:param object: base object where changes should be applied (not in-place).
|
|
515
|
+
:param list changes: list of JSON patch operations.
|
|
516
|
+
:param bool only_data: param to limit the scope of the patch only to 'data'.
|
|
517
|
+
:returns dict data: patched object data.
|
|
518
|
+
dict permissions: patched object permissions
|
|
519
|
+
"""
|
|
520
|
+
data = {**obj}
|
|
521
|
+
|
|
522
|
+
# Permissions should always have read and write fields defined (to allow add)
|
|
523
|
+
permissions = {"read": set(), "write": set()}
|
|
524
|
+
|
|
525
|
+
# Get permissions if available on the resource (using SharableResource)
|
|
526
|
+
permissions.update(data.pop("__permissions__", {}))
|
|
527
|
+
|
|
528
|
+
# Permissions should be mapped as a dict, since jsonpatch doesn't accept
|
|
529
|
+
# sets and lists are mapped as JSON arrays (not indexed by value)
|
|
530
|
+
permissions = {k: {i: i for i in v} for k, v in permissions.items()}
|
|
531
|
+
|
|
532
|
+
resource = {"data": data, "permissions": permissions}
|
|
533
|
+
|
|
534
|
+
# Allow patch permissions without value since key and value are equal on sets
|
|
535
|
+
for op in ops:
|
|
536
|
+
# 'path' is here since it was validated.
|
|
537
|
+
if op["path"].startswith(("/permissions/read/", "/permissions/write/")):
|
|
538
|
+
op["value"] = op["path"].split("/")[-1]
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
result = jsonpatch.apply_patch(resource, ops)
|
|
542
|
+
|
|
543
|
+
except (jsonpatch.JsonPatchException, jsonpatch.JsonPointerException) as e:
|
|
544
|
+
raise ValueError(e)
|
|
545
|
+
|
|
546
|
+
return result
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def safe_wraps(wrapper, *args, **kwargs):
|
|
550
|
+
"""Safely wraps partial functions."""
|
|
551
|
+
while isinstance(wrapper, functools.partial):
|
|
552
|
+
wrapper = wrapper.func
|
|
553
|
+
return functools.wraps(wrapper, *args, **kwargs)
|
|
File without changes
|