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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from pyramid.authorization import Authenticated
|
|
5
|
+
from pyramid.interfaces import IAuthorizationPolicy
|
|
6
|
+
from pyramid.settings import aslist
|
|
7
|
+
from zope.interface import implementer
|
|
8
|
+
|
|
9
|
+
from kinto.core import utils
|
|
10
|
+
from kinto.core.storage import exceptions as storage_exceptions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# When permission is set to "private", only the current user is allowed.
|
|
16
|
+
PRIVATE = "private"
|
|
17
|
+
|
|
18
|
+
# A permission is called "dynamic" when it's computed at request time.
|
|
19
|
+
DYNAMIC = "dynamic"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def groupfinder(userid, request):
|
|
23
|
+
"""Fetch principals from permission backend for the specified `userid`.
|
|
24
|
+
|
|
25
|
+
This is plugged by default using the ``multiauth.groupfinder`` setting.
|
|
26
|
+
"""
|
|
27
|
+
backend = getattr(request.registry, "permission", None)
|
|
28
|
+
# Permission backend not configured. Ignore.
|
|
29
|
+
if not backend:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
# Safety check when Kinto-Core is used without pyramid_multiauth.
|
|
33
|
+
if request.prefixed_userid:
|
|
34
|
+
userid = request.prefixed_userid
|
|
35
|
+
|
|
36
|
+
# Query the permission backend only once per request (e.g. batch).
|
|
37
|
+
reify_key = userid + "_principals"
|
|
38
|
+
if reify_key not in request.bound_data:
|
|
39
|
+
principals = backend.get_user_principals(userid)
|
|
40
|
+
request.bound_data[reify_key] = principals
|
|
41
|
+
|
|
42
|
+
return request.bound_data[reify_key]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@implementer(IAuthorizationPolicy)
|
|
46
|
+
class AuthorizationPolicy:
|
|
47
|
+
"""Default authorization class, that leverages the permission backend
|
|
48
|
+
for shareable resources.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
get_bound_permissions = None
|
|
52
|
+
"""Callable that takes an object id and a permission and returns
|
|
53
|
+
a list of tuples (<object id>, <permission>). Useful when objects
|
|
54
|
+
permission depend on others."""
|
|
55
|
+
|
|
56
|
+
def permits(self, context, principals, permission):
|
|
57
|
+
if permission == PRIVATE:
|
|
58
|
+
# When using the private permission, we bypass the permissions
|
|
59
|
+
# backend, and simply authorize if authenticated.
|
|
60
|
+
return Authenticated in principals
|
|
61
|
+
|
|
62
|
+
principals = context.get_prefixed_principals()
|
|
63
|
+
|
|
64
|
+
create_permission = f"{context.resource_name}:create"
|
|
65
|
+
|
|
66
|
+
permission = context.required_permission
|
|
67
|
+
if permission == "create":
|
|
68
|
+
permission = create_permission
|
|
69
|
+
|
|
70
|
+
object_id = context.permission_object_id
|
|
71
|
+
bound_perms = self._get_bound_permissions(object_id, permission)
|
|
72
|
+
|
|
73
|
+
allowed = context.check_permission(principals, bound_perms)
|
|
74
|
+
|
|
75
|
+
# Here we consider that parent URI is one path level above.
|
|
76
|
+
parent_uri = "/".join(object_id.split("/")[:-1]) if object_id else None
|
|
77
|
+
|
|
78
|
+
# If not allowed to delete/patch, and target object is missing, and
|
|
79
|
+
# allowed to read the parent, then view is permitted (will raise 404
|
|
80
|
+
# later anyway). See Kinto/kinto#918
|
|
81
|
+
is_object_unknown = not context.on_plural_endpoint and context.current_object is None
|
|
82
|
+
if context.required_permission == "write" and is_object_unknown:
|
|
83
|
+
bound_perms = self._get_bound_permissions(parent_uri, "read")
|
|
84
|
+
allowed = context.check_permission(principals, bound_perms)
|
|
85
|
+
|
|
86
|
+
# If not allowed on this plural endpoint, but some objects are shared with
|
|
87
|
+
# the current user, then authorize.
|
|
88
|
+
# The :class:`kinto.core.resource.Resource` class will take care of the filtering.
|
|
89
|
+
is_list_operation = (
|
|
90
|
+
context.on_plural_endpoint
|
|
91
|
+
and not permission.endswith("create")
|
|
92
|
+
and context.current_object is None
|
|
93
|
+
)
|
|
94
|
+
if not allowed and is_list_operation:
|
|
95
|
+
allowed = bool(
|
|
96
|
+
context.fetch_shared_objects(permission, principals, self.get_bound_permissions)
|
|
97
|
+
)
|
|
98
|
+
if not allowed:
|
|
99
|
+
# If allowed to create this kind of object on parent,
|
|
100
|
+
# then allow to obtain the list.
|
|
101
|
+
if len(bound_perms) > 0:
|
|
102
|
+
bound_perms = [(parent_uri, create_permission)]
|
|
103
|
+
else:
|
|
104
|
+
bound_perms = [("", "create")] # Root object.
|
|
105
|
+
allowed = context.check_permission(principals, bound_perms)
|
|
106
|
+
|
|
107
|
+
if not allowed:
|
|
108
|
+
logger.info(
|
|
109
|
+
"Permission %r on %r not granted to %r.",
|
|
110
|
+
permission,
|
|
111
|
+
object_id,
|
|
112
|
+
principals[0],
|
|
113
|
+
extra=dict(userid=principals[0], uri=object_id, perm=permission),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return allowed
|
|
117
|
+
|
|
118
|
+
def _get_bound_permissions(self, object_id, permission):
|
|
119
|
+
if self.get_bound_permissions is None:
|
|
120
|
+
# Permission to 'write' gives permission to 'read'.
|
|
121
|
+
bound = [(object_id, permission)]
|
|
122
|
+
if permission == "read":
|
|
123
|
+
bound += [(object_id, "write")]
|
|
124
|
+
return bound
|
|
125
|
+
return self.get_bound_permissions(object_id, permission)
|
|
126
|
+
|
|
127
|
+
def principals_allowed_by_permission(self, context, permission):
|
|
128
|
+
raise NotImplementedError() # PRAGMA NOCOVER
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class RouteFactory:
|
|
132
|
+
resource_name = None
|
|
133
|
+
on_plural_endpoint = False
|
|
134
|
+
required_permission = None
|
|
135
|
+
permission_object_id = None
|
|
136
|
+
current_object = None
|
|
137
|
+
shared_ids = None
|
|
138
|
+
|
|
139
|
+
method_permissions = {
|
|
140
|
+
"head": "read",
|
|
141
|
+
"get": "read",
|
|
142
|
+
"post": "create",
|
|
143
|
+
"delete": "write",
|
|
144
|
+
"patch": "write",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def __init__(self, request):
|
|
148
|
+
# Store some shortcuts.
|
|
149
|
+
permission = request.registry.permission
|
|
150
|
+
self._check_permission = permission.check_permission
|
|
151
|
+
self._get_accessible_objects = permission.get_accessible_objects
|
|
152
|
+
|
|
153
|
+
self.get_prefixed_principals = functools.partial(utils.prefixed_principals, request)
|
|
154
|
+
|
|
155
|
+
# Store current resource and required permission.
|
|
156
|
+
service = utils.current_service(request)
|
|
157
|
+
is_on_resource = (
|
|
158
|
+
service is not None and hasattr(service, "viewset") and hasattr(service, "resource")
|
|
159
|
+
)
|
|
160
|
+
self._resource = None
|
|
161
|
+
if is_on_resource:
|
|
162
|
+
self.resource_name = request.current_resource_name
|
|
163
|
+
self.on_plural_endpoint = getattr(service, "type", None) == "plural"
|
|
164
|
+
|
|
165
|
+
# Check if this request targets an individual object.
|
|
166
|
+
# Its existence will affect permissions checking (cf `_find_required_permission()`).
|
|
167
|
+
# There are cases where the permission is not directly related to the HTTP method,
|
|
168
|
+
# For example:
|
|
169
|
+
# - with POST on plural endpoint, with an id supplied
|
|
170
|
+
# - with PUT on an object, which can either be creation or update
|
|
171
|
+
is_write_on_object = not self.on_plural_endpoint and request.method.lower() in (
|
|
172
|
+
"put",
|
|
173
|
+
"delete",
|
|
174
|
+
"patch",
|
|
175
|
+
)
|
|
176
|
+
is_post_on_plural = self.on_plural_endpoint and request.method.lower() == "post"
|
|
177
|
+
if is_write_on_object or is_post_on_plural:
|
|
178
|
+
# We instantiate the resource to determine the object targeted by the request.
|
|
179
|
+
self._resource = resource = service.resource(request=request, context=self)
|
|
180
|
+
if resource.object_id is not None: # Skip POST on plural without id.
|
|
181
|
+
try:
|
|
182
|
+
# Save a reference, to avoid refetching from storage in resource.
|
|
183
|
+
self.current_object = resource.model.get_object(resource.object_id)
|
|
184
|
+
except storage_exceptions.ObjectNotFoundError:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
self.permission_object_id, self.required_permission = self._find_required_permission(
|
|
188
|
+
request, service
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# To obtain shared objects on a plural endpoint, use a match:
|
|
192
|
+
self._object_id_match = self.get_permission_object_id(request, "*")
|
|
193
|
+
|
|
194
|
+
self._settings = request.registry.settings
|
|
195
|
+
|
|
196
|
+
def check_permission(self, principals, bound_perms):
|
|
197
|
+
"""Read allowed principals from settings, if not any, query the permission
|
|
198
|
+
backend to check if view is allowed.
|
|
199
|
+
"""
|
|
200
|
+
if not bound_perms:
|
|
201
|
+
bound_perms = [(self.resource_name, self.required_permission)]
|
|
202
|
+
for _, permission in bound_perms:
|
|
203
|
+
# With Kinto inheritance tree, we can have: `permission = "record:create"`
|
|
204
|
+
if self.resource_name and permission.startswith(self.resource_name):
|
|
205
|
+
setting = f"{permission.replace(':', '_')}_principals"
|
|
206
|
+
else:
|
|
207
|
+
setting = f"{self.resource_name}_{permission}_principals"
|
|
208
|
+
allowed_principals = aslist(self._settings.get(setting, ""))
|
|
209
|
+
if allowed_principals:
|
|
210
|
+
if bool(set(allowed_principals) & set(principals)):
|
|
211
|
+
return True
|
|
212
|
+
return self._check_permission(principals, bound_perms)
|
|
213
|
+
|
|
214
|
+
def fetch_shared_objects(self, perm, principals, get_bound_permissions):
|
|
215
|
+
"""Fetch objects that are readable or writable for the current
|
|
216
|
+
principals.
|
|
217
|
+
|
|
218
|
+
See :meth:`kinto.core.authorization.AuthorizationPolicy.permits`
|
|
219
|
+
|
|
220
|
+
If no object is shared, it returns None.
|
|
221
|
+
|
|
222
|
+
.. warning::
|
|
223
|
+
This sets the ``shared_ids`` attribute to the context with the
|
|
224
|
+
return value. The attribute is then read by
|
|
225
|
+
:class:`kinto.core.resource.Resource`
|
|
226
|
+
"""
|
|
227
|
+
if get_bound_permissions:
|
|
228
|
+
bound_perms = get_bound_permissions(self._object_id_match, perm)
|
|
229
|
+
else:
|
|
230
|
+
bound_perms = [(self._object_id_match, perm)]
|
|
231
|
+
by_obj_id = self._get_accessible_objects(principals, bound_perms, with_children=False)
|
|
232
|
+
ids = by_obj_id.keys()
|
|
233
|
+
# Store for later use in ``Resource``.
|
|
234
|
+
self.shared_ids = [self._extract_object_id(id_) for id_ in ids]
|
|
235
|
+
return self.shared_ids
|
|
236
|
+
|
|
237
|
+
def get_permission_object_id(self, request, object_id=None):
|
|
238
|
+
"""Returns the permission object id for the current request.
|
|
239
|
+
In the nominal case, it is just the current URI without version prefix.
|
|
240
|
+
For plural endpoint, it is the related object URI using the specified
|
|
241
|
+
`object_id`.
|
|
242
|
+
|
|
243
|
+
See :meth:`kinto.core.resource.model.SharableModel` and
|
|
244
|
+
:meth:`kinto.core.authorization.RouteFactory.__init__`
|
|
245
|
+
"""
|
|
246
|
+
object_uri = utils.strip_uri_prefix(request.path)
|
|
247
|
+
|
|
248
|
+
if self.on_plural_endpoint and object_id is not None:
|
|
249
|
+
# With the current request on a plural endpoint, the object URI must
|
|
250
|
+
# be found out by inspecting the "plural" service and its sibling
|
|
251
|
+
# "object" service. (see `register_resource()`)
|
|
252
|
+
matchdict = {**request.matchdict, "id": object_id}
|
|
253
|
+
try:
|
|
254
|
+
object_uri = utils.instance_uri(request, self.resource_name, **matchdict)
|
|
255
|
+
object_uri = object_uri.replace("%2A", "*")
|
|
256
|
+
except KeyError:
|
|
257
|
+
# Maybe the resource has no single object endpoint.
|
|
258
|
+
# We consider that object URIs in permissions backend will
|
|
259
|
+
# be stored naively:
|
|
260
|
+
object_uri = f"{object_uri}/{object_id}"
|
|
261
|
+
|
|
262
|
+
return object_uri
|
|
263
|
+
|
|
264
|
+
def _extract_object_id(self, object_uri):
|
|
265
|
+
# XXX: Rewrite using kinto.core.utils.view_lookup() and matchdict['id']
|
|
266
|
+
return object_uri.split("/")[-1]
|
|
267
|
+
|
|
268
|
+
def _find_required_permission(self, request, service):
|
|
269
|
+
"""Find out what is the permission object id and the required
|
|
270
|
+
permission.
|
|
271
|
+
|
|
272
|
+
.. note::
|
|
273
|
+
This method saves an attribute ``self.current_object`` used
|
|
274
|
+
in :class:`kinto.core.resource.Resource`.
|
|
275
|
+
"""
|
|
276
|
+
# By default, it's a URI a and permission associated to the method.
|
|
277
|
+
permission_object_id = self.get_permission_object_id(request)
|
|
278
|
+
method = request.method.lower()
|
|
279
|
+
required_permission = self.method_permissions.get(method)
|
|
280
|
+
|
|
281
|
+
# For create permission, the object id is the plural endpoint.
|
|
282
|
+
plural_path = str(service.plural_path)
|
|
283
|
+
plural_path = plural_path.format_map(request.matchdict)
|
|
284
|
+
|
|
285
|
+
# In the case of a "PUT", check if the targetted object already
|
|
286
|
+
# exists, return "write" if it does, "create" otherwise.
|
|
287
|
+
if request.method.lower() == "put":
|
|
288
|
+
if self.current_object is None:
|
|
289
|
+
# The object does not exist, the permission to create on
|
|
290
|
+
# the related plural endpoint is required.
|
|
291
|
+
permission_object_id = plural_path
|
|
292
|
+
required_permission = "create"
|
|
293
|
+
else:
|
|
294
|
+
# For safe creations, the user needs a create permission.
|
|
295
|
+
# See Kinto/kinto#792
|
|
296
|
+
if request.headers.get("If-None-Match") == "*":
|
|
297
|
+
permission_object_id = plural_path
|
|
298
|
+
required_permission = "create"
|
|
299
|
+
else:
|
|
300
|
+
required_permission = "write"
|
|
301
|
+
|
|
302
|
+
# In the case of a "POST" on a plural endpoint, if an "id" was
|
|
303
|
+
# specified, then the object is returned. The required permission
|
|
304
|
+
# is thus "read" on this object.
|
|
305
|
+
if request.method.lower() == "post" and self.current_object is not None:
|
|
306
|
+
permission_object_id = self.get_permission_object_id(
|
|
307
|
+
request, object_id=self._resource.object_id
|
|
308
|
+
)
|
|
309
|
+
required_permission = "read"
|
|
310
|
+
|
|
311
|
+
return (permission_object_id, required_permission)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_HEARTBEAT_DELETE_RATE = 0.5
|
|
9
|
+
_HEARTBEAT_KEY = "__heartbeat__"
|
|
10
|
+
_HEARTBEAT_TTL_SECONDS = 3600
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_CACHE_HIT_METRIC_KEY = "cache_hits"
|
|
14
|
+
_CACHE_MISS_METRIC_KEY = "cache_misses"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CacheBase:
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
self.prefix = kwargs["cache_prefix"]
|
|
20
|
+
self.max_size_bytes = kwargs.get("cache_max_size_bytes")
|
|
21
|
+
self.set_metrics_backend(kwargs.get("metrics_backend"))
|
|
22
|
+
|
|
23
|
+
def initialize_schema(self, dry_run=False):
|
|
24
|
+
"""Create every necessary objects (like tables or indices) in the
|
|
25
|
+
backend.
|
|
26
|
+
|
|
27
|
+
This is executed when the ``kinto migrate`` command is run.
|
|
28
|
+
|
|
29
|
+
:param bool dry_run: simulate instead of executing the operations.
|
|
30
|
+
"""
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def flush(self):
|
|
34
|
+
"""Delete every values."""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
def ttl(self, key):
|
|
38
|
+
"""Obtain the expiration value of the specified `key`.
|
|
39
|
+
|
|
40
|
+
:param str key: key
|
|
41
|
+
:returns: number of seconds or negative if no TTL.
|
|
42
|
+
:rtype: float
|
|
43
|
+
"""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def expire(self, key, ttl):
|
|
47
|
+
"""Set the expiration value `ttl` for the specified `key`.
|
|
48
|
+
|
|
49
|
+
:param str key: key
|
|
50
|
+
:param float ttl: number of seconds
|
|
51
|
+
"""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def set(self, key, value, ttl):
|
|
55
|
+
"""Store a value with the specified `key`.
|
|
56
|
+
|
|
57
|
+
:param str key: key
|
|
58
|
+
:param str value: value to store
|
|
59
|
+
:param float ttl: expire after number of seconds
|
|
60
|
+
"""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def get(self, key):
|
|
64
|
+
"""Obtain the value of the specified `key`.
|
|
65
|
+
|
|
66
|
+
:param str key: key
|
|
67
|
+
:returns: the stored value or None if missing.
|
|
68
|
+
:rtype: str
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def delete(self, key):
|
|
73
|
+
"""Delete the value of the specified `key`.
|
|
74
|
+
|
|
75
|
+
:param str key: key
|
|
76
|
+
"""
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
def set_metrics_backend(self, metrics_backend):
|
|
80
|
+
"""Set a metrics backend via the `CacheMetricsBackend` adapter.
|
|
81
|
+
|
|
82
|
+
:param metrics_backend: A metrics backend implementing the IMetricsService interface.
|
|
83
|
+
"""
|
|
84
|
+
self.metrics_backend = CacheMetricsBackend(metrics_backend)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CacheMetricsBackend:
|
|
88
|
+
"""
|
|
89
|
+
A simple adapter for tracking cache-related metrics.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, metrics_backend, *args, **kwargs):
|
|
93
|
+
"""Initialize with a given metrics backend.
|
|
94
|
+
|
|
95
|
+
:param metrics_backend: A metrics backend implementing the IMetricsService interface.
|
|
96
|
+
"""
|
|
97
|
+
self._backend = metrics_backend
|
|
98
|
+
|
|
99
|
+
def count_hit(self):
|
|
100
|
+
"""Increment the cache hit counter."""
|
|
101
|
+
if self._backend:
|
|
102
|
+
self._backend.count(key=_CACHE_HIT_METRIC_KEY)
|
|
103
|
+
|
|
104
|
+
def count_miss(self):
|
|
105
|
+
"""Increment the cache miss counter."""
|
|
106
|
+
if self._backend:
|
|
107
|
+
self._backend.count(key=_CACHE_MISS_METRIC_KEY)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def heartbeat(backend):
|
|
111
|
+
def ping(request):
|
|
112
|
+
"""Test that cache backend is operational.
|
|
113
|
+
|
|
114
|
+
:param request: current request object
|
|
115
|
+
:type request: :class:`~pyramid:pyramid.request.Request`
|
|
116
|
+
:returns: ``True`` is everything is ok, ``False`` otherwise.
|
|
117
|
+
:rtype: bool
|
|
118
|
+
"""
|
|
119
|
+
# No specific case for readonly mode because the cache should
|
|
120
|
+
# continue to work in that mode.
|
|
121
|
+
try:
|
|
122
|
+
if random.SystemRandom().random() < _HEARTBEAT_DELETE_RATE:
|
|
123
|
+
backend.delete(_HEARTBEAT_KEY)
|
|
124
|
+
return backend.get(_HEARTBEAT_KEY) is None
|
|
125
|
+
backend.set(_HEARTBEAT_KEY, "alive", _HEARTBEAT_TTL_SECONDS)
|
|
126
|
+
return backend.get(_HEARTBEAT_KEY) == "alive"
|
|
127
|
+
except Exception:
|
|
128
|
+
logger.exception("Heartbeat Failure")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
return ping
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from math import ceil, floor
|
|
4
|
+
from time import time
|
|
5
|
+
|
|
6
|
+
from pyramid.settings import aslist
|
|
7
|
+
|
|
8
|
+
from kinto.core.cache import CacheBase
|
|
9
|
+
from kinto.core.storage import exceptions
|
|
10
|
+
from kinto.core.utils import json, memcache
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def wrap_memcached_error(func):
|
|
17
|
+
@wraps(func)
|
|
18
|
+
def wrapped(*args, **kwargs):
|
|
19
|
+
try:
|
|
20
|
+
return func(*args, **kwargs)
|
|
21
|
+
except TypeError:
|
|
22
|
+
raise
|
|
23
|
+
except (
|
|
24
|
+
memcache.Client.MemcachedKeyError,
|
|
25
|
+
memcache.Client.MemcachedStringEncodingError,
|
|
26
|
+
) as e:
|
|
27
|
+
logger.exception(e)
|
|
28
|
+
raise exceptions.BackendError(original=e)
|
|
29
|
+
|
|
30
|
+
return wrapped
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_from_config(config, prefix=""):
|
|
34
|
+
"""Memcached client instantiation from settings."""
|
|
35
|
+
settings = config.get_settings()
|
|
36
|
+
hosts = aslist(settings[prefix + "hosts"])
|
|
37
|
+
return memcache.Client(hosts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Cache(CacheBase):
|
|
41
|
+
"""Cache backend implementation using Memcached.
|
|
42
|
+
|
|
43
|
+
Enable in configuration::
|
|
44
|
+
|
|
45
|
+
kinto.cache_backend = kinto.core.cache.memcached
|
|
46
|
+
|
|
47
|
+
*(Optional)* Instance location URI can be customized::
|
|
48
|
+
|
|
49
|
+
kinto.cache_hosts = 127.0.0.1:11211 127.0.0.1:11212
|
|
50
|
+
|
|
51
|
+
:noindex:
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, client, *args, **kwargs):
|
|
56
|
+
super(Cache, self).__init__(*args, **kwargs)
|
|
57
|
+
self._client = client
|
|
58
|
+
|
|
59
|
+
def initialize_schema(self, dry_run=False):
|
|
60
|
+
# Nothing to do.
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@wrap_memcached_error
|
|
64
|
+
def flush(self):
|
|
65
|
+
self._client.flush_all()
|
|
66
|
+
|
|
67
|
+
@wrap_memcached_error
|
|
68
|
+
def _get(self, key):
|
|
69
|
+
value = self._client.get(self.prefix + key)
|
|
70
|
+
if not value:
|
|
71
|
+
self.metrics_backend.count_miss()
|
|
72
|
+
return None, 0
|
|
73
|
+
self.metrics_backend.count_hit()
|
|
74
|
+
data = json.loads(value)
|
|
75
|
+
return data["value"], data["ttl"]
|
|
76
|
+
|
|
77
|
+
def ttl(self, key):
|
|
78
|
+
_, ttl = self._get(key)
|
|
79
|
+
val = ttl - time()
|
|
80
|
+
return floor(val)
|
|
81
|
+
|
|
82
|
+
def get(self, key):
|
|
83
|
+
value, _ = self._get(key)
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
@wrap_memcached_error
|
|
87
|
+
def expire(self, key, ttl):
|
|
88
|
+
if ttl == 0:
|
|
89
|
+
self.delete(key)
|
|
90
|
+
else:
|
|
91
|
+
# We can't use touch here because we need to update the TTL value in the record.
|
|
92
|
+
value = self.get(key)
|
|
93
|
+
self.set(key, value, ttl)
|
|
94
|
+
|
|
95
|
+
@wrap_memcached_error
|
|
96
|
+
def set(self, key, value, ttl):
|
|
97
|
+
if isinstance(value, bytes):
|
|
98
|
+
raise TypeError("a string-like object is required, not 'bytes'")
|
|
99
|
+
value = json.dumps({"value": value, "ttl": ceil(time() + ttl)})
|
|
100
|
+
self._client.set(self.prefix + key, value, int(ttl))
|
|
101
|
+
|
|
102
|
+
@wrap_memcached_error
|
|
103
|
+
def delete(self, key):
|
|
104
|
+
value = self.get(key)
|
|
105
|
+
self._client.delete(self.prefix + key)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_from_config(config):
|
|
110
|
+
settings = config.get_settings()
|
|
111
|
+
client = create_from_config(config, prefix="cache_")
|
|
112
|
+
return Cache(client, cache_prefix=settings["cache_prefix"])
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from kinto.core.cache import CacheBase
|
|
4
|
+
from kinto.core.decorators import synchronized
|
|
5
|
+
from kinto.core.utils import msec_time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Cache(CacheBase):
|
|
12
|
+
"""Cache backend implementation in local process memory.
|
|
13
|
+
|
|
14
|
+
Enable in configuration::
|
|
15
|
+
|
|
16
|
+
kinto.cache_backend = kinto.core.cache.memory
|
|
17
|
+
|
|
18
|
+
:noindex:
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self.flush()
|
|
24
|
+
|
|
25
|
+
def initialize_schema(self, dry_run=False):
|
|
26
|
+
# Nothing to do.
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def flush(self):
|
|
30
|
+
self._created_at = {}
|
|
31
|
+
self._ttl = {}
|
|
32
|
+
self._store = {}
|
|
33
|
+
self._quota = 0
|
|
34
|
+
|
|
35
|
+
def _clean_expired(self):
|
|
36
|
+
current = msec_time()
|
|
37
|
+
expired = [k for k, v in self._ttl.items() if current >= v]
|
|
38
|
+
for expired_item_key in expired:
|
|
39
|
+
self.delete(expired_item_key[len(self.prefix) :])
|
|
40
|
+
|
|
41
|
+
def _clean_oversized(self):
|
|
42
|
+
if self._quota < self.max_size_bytes:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
for key, value in sorted(self._created_at.items(), key=lambda k: k[1]):
|
|
46
|
+
if self._quota < (self.max_size_bytes * 0.8):
|
|
47
|
+
break
|
|
48
|
+
self.delete(key[len(self.prefix) :])
|
|
49
|
+
|
|
50
|
+
@synchronized
|
|
51
|
+
def ttl(self, key):
|
|
52
|
+
ttl = self._ttl.get(self.prefix + key)
|
|
53
|
+
if ttl is not None:
|
|
54
|
+
return (ttl - msec_time()) / 1000.0
|
|
55
|
+
return -1
|
|
56
|
+
|
|
57
|
+
@synchronized
|
|
58
|
+
def expire(self, key, ttl):
|
|
59
|
+
self._ttl[self.prefix + key] = msec_time() + int(ttl * 1000.0)
|
|
60
|
+
|
|
61
|
+
@synchronized
|
|
62
|
+
def set(self, key, value, ttl):
|
|
63
|
+
if isinstance(value, bytes):
|
|
64
|
+
raise TypeError("a string-like object is required, not 'bytes'")
|
|
65
|
+
self._clean_expired()
|
|
66
|
+
self._clean_oversized()
|
|
67
|
+
self.expire(key, ttl)
|
|
68
|
+
item_key = self.prefix + key
|
|
69
|
+
self._store[item_key] = value
|
|
70
|
+
self._created_at[item_key] = msec_time()
|
|
71
|
+
self._quota += size_of(item_key, value)
|
|
72
|
+
|
|
73
|
+
@synchronized
|
|
74
|
+
def get(self, key):
|
|
75
|
+
self._clean_expired()
|
|
76
|
+
value = self._store.get(self.prefix + key)
|
|
77
|
+
if value is None:
|
|
78
|
+
self.metrics_backend.count_miss()
|
|
79
|
+
return None
|
|
80
|
+
self.metrics_backend.count_hit()
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
@synchronized
|
|
84
|
+
def delete(self, key):
|
|
85
|
+
key = self.prefix + key
|
|
86
|
+
self._ttl.pop(key, None)
|
|
87
|
+
self._created_at.pop(key, None)
|
|
88
|
+
value = self._store.pop(key, None)
|
|
89
|
+
self._quota -= size_of(key, value)
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_from_config(config):
|
|
94
|
+
settings = config.get_settings()
|
|
95
|
+
return Cache(
|
|
96
|
+
cache_prefix=settings["cache_prefix"],
|
|
97
|
+
cache_max_size_bytes=settings["cache_max_size_bytes"],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def size_of(key, value):
|
|
102
|
+
# Key used for ttl, created_at and store.
|
|
103
|
+
# Int size is 24 bytes one for ttl and one for created_at values
|
|
104
|
+
return len(key) * 3 + len(str(value)) + 24 * 2
|