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/metrics.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import types
|
|
2
|
+
|
|
3
|
+
from zope.interface import Interface, implementer
|
|
4
|
+
|
|
5
|
+
from kinto.core import utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IMetricsService(Interface):
|
|
9
|
+
"""
|
|
10
|
+
An interface that defines the metrics service contract.
|
|
11
|
+
Any class implementing this must provide all its methods.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def timer(key):
|
|
15
|
+
"""
|
|
16
|
+
Watch execution time in seconds.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def observe(self, key, value, labels=[]):
|
|
20
|
+
"""
|
|
21
|
+
Observe a give `value` for the specified `key`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def count(key, count=1, unique=None):
|
|
25
|
+
"""
|
|
26
|
+
Count occurrences. If `unique` is set, overwrites the counter value
|
|
27
|
+
on each call.
|
|
28
|
+
|
|
29
|
+
`unique` should be of type ``list[tuple[str,str]]``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NoOpTimer:
|
|
34
|
+
def __call__(self, f):
|
|
35
|
+
@utils.safe_wraps(f)
|
|
36
|
+
def _wrapped(*args, **kwargs):
|
|
37
|
+
return f(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
return _wrapped
|
|
40
|
+
|
|
41
|
+
def __enter__(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *args, **kwargs):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@implementer(IMetricsService)
|
|
49
|
+
class NoOpMetricsService:
|
|
50
|
+
def timer(self, key, value=None, labels=[]):
|
|
51
|
+
return NoOpTimer()
|
|
52
|
+
|
|
53
|
+
def observe(self, key, value, labels=[]):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def count(self, key, count=1, unique=None):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def watch_execution_time(metrics_service, obj, prefix="", classname=None):
|
|
61
|
+
"""
|
|
62
|
+
Decorate all methods of an object in order to watch their execution time.
|
|
63
|
+
Metrics will be named `{prefix}.{classname}.{method}`.
|
|
64
|
+
"""
|
|
65
|
+
classname = classname or utils.classname(obj)
|
|
66
|
+
members = dir(obj)
|
|
67
|
+
for name in members:
|
|
68
|
+
method = getattr(obj, name)
|
|
69
|
+
is_method = isinstance(method, types.MethodType)
|
|
70
|
+
if not name.startswith("_") and is_method:
|
|
71
|
+
metric_name = f"{prefix}.{classname}.seconds"
|
|
72
|
+
labels = [("method", name)]
|
|
73
|
+
decorated_method = metrics_service.timer(metric_name, labels=labels)(method)
|
|
74
|
+
setattr(obj, name, decorated_method)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def listener_with_timer(config, key, func):
|
|
78
|
+
"""
|
|
79
|
+
Add a timer with the specified `key` on the specified `func`.
|
|
80
|
+
This is used to avoid evaluating `config.registry.metrics` during setup time
|
|
81
|
+
to avoid having to deal with initialization order and configuration committing.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def wrapped(*args, **kwargs):
|
|
85
|
+
metrics_service = config.registry.metrics
|
|
86
|
+
if not metrics_service:
|
|
87
|
+
# This only happens if `kinto.core.initialization.setup_metrics` is
|
|
88
|
+
# not listed in the `initialization_sequence` setting.
|
|
89
|
+
return func(*args, **kwargs)
|
|
90
|
+
# If metrics are enabled, monitor execution time of listeners.
|
|
91
|
+
with metrics_service.timer(key + ".seconds" if not key.endswith(".seconds") else key):
|
|
92
|
+
return func(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return wrapped
|
kinto/core/openapi.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from kinto.core.cornice_swagger import CorniceSwagger
|
|
2
|
+
from kinto.core.cornice_swagger.converters.schema import TypeConverter
|
|
3
|
+
from kinto.core.schema import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnyTypeConverter(TypeConverter):
|
|
7
|
+
"""Convert type agnostic parameter to swagger."""
|
|
8
|
+
|
|
9
|
+
def __call__(self, schema_node):
|
|
10
|
+
return {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAPI(CorniceSwagger):
|
|
14
|
+
"""OpenAPI documentation generator."""
|
|
15
|
+
|
|
16
|
+
custom_type_converters = {Any: AnyTypeConverter}
|
|
17
|
+
"""Kinto additional type converters."""
|
|
18
|
+
|
|
19
|
+
security_definitions = {}
|
|
20
|
+
"""Kinto security definitions. May be used for setting other security methods."""
|
|
21
|
+
|
|
22
|
+
security_roles = {}
|
|
23
|
+
"""Kinto resource security roles. May be used for setting OAuth roles by plugins."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def expose_authentication_method(cls, method_name, definition):
|
|
27
|
+
"""Allow security extensions to expose authentication methods on the
|
|
28
|
+
OpenAPI documentation. The definition field should correspond to a
|
|
29
|
+
valid OpenAPI security definition. Refer the OpenAPI 2.0 specification
|
|
30
|
+
for more information.
|
|
31
|
+
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
|
|
32
|
+
|
|
33
|
+
Below are some examples for BasicAuth and OAuth2::
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
"type": "basic",
|
|
37
|
+
"description" "My basicauth method."
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
"type": "oauth2",
|
|
43
|
+
"authorizationUrl": "https://oauth-stable.dev.lcip.org/v1",
|
|
44
|
+
"flow": "implicit",
|
|
45
|
+
"scopes": {"kinto": "Kinto user scope."}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
cls.security_definitions[method_name] = definition
|
|
50
|
+
cls.security_roles[method_name] = definition.get("scopes", {}).keys()
|
|
51
|
+
|
|
52
|
+
def __init__(self, services, request):
|
|
53
|
+
super().__init__(services)
|
|
54
|
+
|
|
55
|
+
self.request = request
|
|
56
|
+
self.settings = request.registry.settings
|
|
57
|
+
|
|
58
|
+
self.api_title = self.settings["project_name"]
|
|
59
|
+
self.api_version = self.settings["http_api_version"]
|
|
60
|
+
self.ignore_ctypes = ["application/json-patch+json"]
|
|
61
|
+
|
|
62
|
+
# Matches the base routing address - See kinto.core.initialization
|
|
63
|
+
self.base_path = f"/v{self.api_version.split('.')[0]}"
|
|
64
|
+
|
|
65
|
+
def generate(self):
|
|
66
|
+
base_spec = {
|
|
67
|
+
"host": self.request.host,
|
|
68
|
+
"schemes": [self.settings.get("http_scheme") or "http"],
|
|
69
|
+
"securityDefinitions": self.security_definitions,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return super(OpenAPI, self).generate(swagger=base_spec)
|
|
73
|
+
|
|
74
|
+
def default_tags(self, service, method):
|
|
75
|
+
"""Povides default tags to views."""
|
|
76
|
+
|
|
77
|
+
base_tag = service.name.capitalize()
|
|
78
|
+
base_tag = base_tag.replace("-plural", "s")
|
|
79
|
+
base_tag = base_tag.replace("-object", "s")
|
|
80
|
+
|
|
81
|
+
return [base_tag]
|
|
82
|
+
|
|
83
|
+
def default_op_ids(self, service, method):
|
|
84
|
+
"""Povides default operation ids to methods if not defined on view."""
|
|
85
|
+
|
|
86
|
+
method = method.lower()
|
|
87
|
+
method_mapping = {"post": "create", "put": "update"}
|
|
88
|
+
if method in method_mapping:
|
|
89
|
+
method = method_mapping[method]
|
|
90
|
+
|
|
91
|
+
resource = service.name
|
|
92
|
+
if method == "create":
|
|
93
|
+
resource = resource.replace("-plural", "")
|
|
94
|
+
|
|
95
|
+
resource = resource.replace("-plural", "s")
|
|
96
|
+
resource = resource.replace("-object", "")
|
|
97
|
+
op_id = f"{method}_{resource}"
|
|
98
|
+
|
|
99
|
+
return op_id
|
|
100
|
+
|
|
101
|
+
def default_security(self, service, method):
|
|
102
|
+
"""Provides OpenAPI security properties based on kinto policies."""
|
|
103
|
+
|
|
104
|
+
definitions = service.definitions
|
|
105
|
+
|
|
106
|
+
# Get method view arguments
|
|
107
|
+
for definition in definitions: # pragma: no branch
|
|
108
|
+
met, view, args = definition
|
|
109
|
+
if met == method:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if args.get("permission") == "__no_permission_required__":
|
|
113
|
+
return []
|
|
114
|
+
else:
|
|
115
|
+
return [{name: list(roles)} for name, roles in self.security_roles.items()]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from pyramid.settings import asbool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__HEARTBEAT_KEY__ = "__heartbeat__"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionBase:
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def initialize_schema(self, dry_run=False):
|
|
17
|
+
"""Create every necessary objects (like tables or indices) in the
|
|
18
|
+
backend.
|
|
19
|
+
|
|
20
|
+
This is executed with the ``kinto migrate`` command.
|
|
21
|
+
|
|
22
|
+
:param bool dry_run: simulate instead of executing the operations.
|
|
23
|
+
"""
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def flush(self):
|
|
27
|
+
"""Delete all data stored in the permission backend."""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def add_user_principal(self, user_id, principal):
|
|
31
|
+
"""Add an additional principal to a user.
|
|
32
|
+
|
|
33
|
+
:param str user_id: The user_id to add the principal to.
|
|
34
|
+
:param str principal: The principal to add.
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
def remove_user_principal(self, user_id, principal):
|
|
39
|
+
"""Remove an additional principal from a user.
|
|
40
|
+
|
|
41
|
+
:param str user_id: The user_id to remove the principal to.
|
|
42
|
+
:param str principal: The principal to remove.
|
|
43
|
+
"""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def remove_principal(self, principal):
|
|
47
|
+
"""Remove a principal from every user.
|
|
48
|
+
|
|
49
|
+
:param str principal: The principal to remove.
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
def get_user_principals(self, user_id):
|
|
54
|
+
"""Return the set of additionnal principals given to a user.
|
|
55
|
+
|
|
56
|
+
:param str user_id: The user_id to get the list of groups for.
|
|
57
|
+
:returns: The list of group principals the user is in.
|
|
58
|
+
:rtype: set
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def add_principal_to_ace(self, object_id, permission, principal):
|
|
64
|
+
"""Add a principal to an Access Control Entry.
|
|
65
|
+
|
|
66
|
+
:param str object_id: The object to add the permission principal to.
|
|
67
|
+
:param str permission: The permission to add the principal to.
|
|
68
|
+
:param str principal: The principal to add to the ACE.
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def remove_principal_from_ace(self, object_id, permission, principal):
|
|
73
|
+
"""Remove a principal to an Access Control Entry.
|
|
74
|
+
|
|
75
|
+
:param str object_id: The object to remove the permission principal to.
|
|
76
|
+
:param str permission: The permission that should be removed.
|
|
77
|
+
:param str principal: The principal to remove to the ACE.
|
|
78
|
+
"""
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
def get_object_permission_principals(self, object_id, permission):
|
|
82
|
+
"""Return the set of principals of a bound permission
|
|
83
|
+
(unbound permission + object id).
|
|
84
|
+
|
|
85
|
+
:param str object_id: The object_id the permission is set to.
|
|
86
|
+
:param str permission: The permission to query.
|
|
87
|
+
:returns: The list of user principals
|
|
88
|
+
:rtype: set
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
def get_accessible_objects(self, principals, bound_permissions=None, with_children=True):
|
|
94
|
+
"""Return the list of objects where the specified `principals`
|
|
95
|
+
have some permissions.
|
|
96
|
+
|
|
97
|
+
If `bound_permissions` parameter is specified, the list is limited to
|
|
98
|
+
the specified object or permissions.
|
|
99
|
+
|
|
100
|
+
:param list principals: List of user principals
|
|
101
|
+
:param list bound_permissions: An optional list of tuples
|
|
102
|
+
(object_id, permission) to limit the results.
|
|
103
|
+
The object ids can be a pattern, (e.g. ``*``, ``'/my/articles*'``).
|
|
104
|
+
:param bool with_children: Include the children of object ids or not.
|
|
105
|
+
|
|
106
|
+
:returns: A mapping whose keys are the object_ids and the values are
|
|
107
|
+
the related list of permissions.
|
|
108
|
+
:rtype: dict
|
|
109
|
+
"""
|
|
110
|
+
raise NotImplementedError
|
|
111
|
+
|
|
112
|
+
def get_authorized_principals(self, bound_permissions):
|
|
113
|
+
"""Return the full set of authorized principals for a list of bound
|
|
114
|
+
permissions (object + permission).
|
|
115
|
+
|
|
116
|
+
:param str object_id: The object_id the permission is set to.
|
|
117
|
+
:param list bound_permissions: An list of tuples
|
|
118
|
+
(object_id, permission) to be fetched.
|
|
119
|
+
:returns: The list of user principals
|
|
120
|
+
:rtype: set
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
raise NotImplementedError
|
|
124
|
+
|
|
125
|
+
def check_permission(self, principals, bound_permissions):
|
|
126
|
+
"""Test if a principal set have got a permission on an object.
|
|
127
|
+
|
|
128
|
+
:param set principals:
|
|
129
|
+
A set of user principals to test the permission against.
|
|
130
|
+
:param list bound_permissions: An list of tuples
|
|
131
|
+
(object_id, permission) to be checked.
|
|
132
|
+
:rtype: bool
|
|
133
|
+
"""
|
|
134
|
+
principals = set(principals)
|
|
135
|
+
authorized = self.get_authorized_principals(bound_permissions)
|
|
136
|
+
return len(authorized & principals) > 0
|
|
137
|
+
|
|
138
|
+
def get_object_permissions(self, object_id, permissions=None):
|
|
139
|
+
return self.get_objects_permissions([object_id], permissions)[0]
|
|
140
|
+
|
|
141
|
+
def get_objects_permissions(self, objects_ids, permissions=None):
|
|
142
|
+
"""Return a list of mapping, for each object id specified, with the
|
|
143
|
+
set of principals for each permission.
|
|
144
|
+
|
|
145
|
+
:param list objects_ids: The list of object_ids.
|
|
146
|
+
:param list permissions: Optional list of permissions to limit the
|
|
147
|
+
results. If not specified, retrieve all.
|
|
148
|
+
:returns: A list of dictionnaries with the list of user principals for
|
|
149
|
+
each object permission.
|
|
150
|
+
:rtype: list
|
|
151
|
+
"""
|
|
152
|
+
raise NotImplementedError
|
|
153
|
+
|
|
154
|
+
def replace_object_permissions(self, object_id, permissions):
|
|
155
|
+
"""Update the set of principals allowed to perform some actions on an
|
|
156
|
+
object.
|
|
157
|
+
|
|
158
|
+
The only update of permissions allowed by Kinto's API is
|
|
159
|
+
complete replacement of some permission (e.g. I don't know who
|
|
160
|
+
was previously allowed to read this object, but now it's Joe,
|
|
161
|
+
Frank, and Paul). This method implements that, completely
|
|
162
|
+
replacing the set of principals who are granted the given
|
|
163
|
+
permissions (while leaving other permissions alone).
|
|
164
|
+
|
|
165
|
+
:param str object_id: The object to replace permissions on.
|
|
166
|
+
:param permissions: A dict of perm -> principals (where
|
|
167
|
+
principals is an iterable of individuals) to be granted on
|
|
168
|
+
this object.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
raise NotImplementedError
|
|
172
|
+
|
|
173
|
+
def delete_object_permissions(self, *object_id_list):
|
|
174
|
+
"""Delete all listed object permissions.
|
|
175
|
+
|
|
176
|
+
:param str object_id: Remove given objects permissions.
|
|
177
|
+
"""
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def heartbeat(backend):
|
|
182
|
+
def ping(request):
|
|
183
|
+
"""Test the permission backend is operational.
|
|
184
|
+
|
|
185
|
+
:param request: current request object
|
|
186
|
+
:type request: :class:`~pyramid:pyramid.request.Request`
|
|
187
|
+
:returns: ``True`` is everything is ok, ``False`` otherwise.
|
|
188
|
+
:rtype: bool
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
if asbool(request.registry.settings.get("readonly")):
|
|
192
|
+
# Do not try to write in readonly mode.
|
|
193
|
+
backend.get_user_principals(__HEARTBEAT_KEY__)
|
|
194
|
+
else:
|
|
195
|
+
backend.add_user_principal(__HEARTBEAT_KEY__, "alive")
|
|
196
|
+
backend.remove_user_principal(__HEARTBEAT_KEY__, "alive")
|
|
197
|
+
except Exception:
|
|
198
|
+
logger.exception("Heartbeat Error")
|
|
199
|
+
return False
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
return ping
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from kinto.core.decorators import synchronized
|
|
4
|
+
from kinto.core.permission import PermissionBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Permission(PermissionBase):
|
|
8
|
+
"""Permission backend implementation in local process memory.
|
|
9
|
+
|
|
10
|
+
Enable in configuration::
|
|
11
|
+
|
|
12
|
+
kinto.permission_backend = kinto.core.permission.memory
|
|
13
|
+
|
|
14
|
+
:noindex:
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.flush()
|
|
20
|
+
|
|
21
|
+
def initialize_schema(self, dry_run=False):
|
|
22
|
+
# Nothing to do.
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def flush(self):
|
|
26
|
+
self._store = {}
|
|
27
|
+
|
|
28
|
+
@synchronized
|
|
29
|
+
def add_user_principal(self, user_id, principal):
|
|
30
|
+
user_key = f"user:{user_id}"
|
|
31
|
+
user_principals = self._store.get(user_key, set())
|
|
32
|
+
user_principals.add(principal)
|
|
33
|
+
self._store[user_key] = user_principals
|
|
34
|
+
|
|
35
|
+
@synchronized
|
|
36
|
+
def remove_user_principal(self, user_id, principal):
|
|
37
|
+
user_key = f"user:{user_id}"
|
|
38
|
+
user_principals = self._store.get(user_key, set())
|
|
39
|
+
try:
|
|
40
|
+
user_principals.remove(principal)
|
|
41
|
+
except KeyError:
|
|
42
|
+
pass
|
|
43
|
+
if len(user_principals) == 0:
|
|
44
|
+
if user_key in self._store:
|
|
45
|
+
del self._store[user_key]
|
|
46
|
+
else:
|
|
47
|
+
self._store[user_key] = user_principals
|
|
48
|
+
|
|
49
|
+
@synchronized
|
|
50
|
+
def remove_principal(self, principal):
|
|
51
|
+
for key, user_principals in self._store.items():
|
|
52
|
+
if not key.startswith("user:"):
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
user_principals.remove(principal)
|
|
56
|
+
except KeyError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@synchronized
|
|
60
|
+
def get_user_principals(self, user_id):
|
|
61
|
+
# Fetch the groups the user is in.
|
|
62
|
+
user_key = f"user:{user_id}"
|
|
63
|
+
members = self._store.get(user_key, set())
|
|
64
|
+
# Fetch the groups system.Authenticated is in.
|
|
65
|
+
group_authenticated = self._store.get("user:system.Authenticated", set())
|
|
66
|
+
return members | group_authenticated
|
|
67
|
+
|
|
68
|
+
@synchronized
|
|
69
|
+
def add_principal_to_ace(self, object_id, permission, principal):
|
|
70
|
+
permission_key = f"permission:{object_id}:{permission}"
|
|
71
|
+
object_permission_principals = self._store.get(permission_key, set())
|
|
72
|
+
object_permission_principals.add(principal)
|
|
73
|
+
self._store[permission_key] = object_permission_principals
|
|
74
|
+
|
|
75
|
+
@synchronized
|
|
76
|
+
def remove_principal_from_ace(self, object_id, permission, principal):
|
|
77
|
+
permission_key = f"permission:{object_id}:{permission}"
|
|
78
|
+
object_permission_principals = self._store.get(permission_key, set())
|
|
79
|
+
try:
|
|
80
|
+
object_permission_principals.remove(principal)
|
|
81
|
+
except KeyError:
|
|
82
|
+
pass
|
|
83
|
+
if len(object_permission_principals) == 0:
|
|
84
|
+
if permission_key in self._store:
|
|
85
|
+
del self._store[permission_key]
|
|
86
|
+
else:
|
|
87
|
+
self._store[permission_key] = object_permission_principals
|
|
88
|
+
|
|
89
|
+
@synchronized
|
|
90
|
+
def get_object_permission_principals(self, object_id, permission):
|
|
91
|
+
permission_key = f"permission:{object_id}:{permission}"
|
|
92
|
+
members = self._store.get(permission_key, set())
|
|
93
|
+
return members
|
|
94
|
+
|
|
95
|
+
@synchronized
|
|
96
|
+
def get_accessible_objects(self, principals, bound_permissions=None, with_children=True):
|
|
97
|
+
principals = set(principals)
|
|
98
|
+
candidates = []
|
|
99
|
+
if bound_permissions is None:
|
|
100
|
+
for key, value in self._store.items():
|
|
101
|
+
if key.startswith("permission:"):
|
|
102
|
+
_, object_id, permission = key.split(":", 2)
|
|
103
|
+
candidates.append((object_id, permission, value))
|
|
104
|
+
else:
|
|
105
|
+
for pattern, perm in bound_permissions:
|
|
106
|
+
id_match = ".*" if with_children else "[^/]+"
|
|
107
|
+
regexp = re.compile(f"^{pattern.replace('*', id_match)}$")
|
|
108
|
+
for key, value in self._store.items():
|
|
109
|
+
if key.endswith(perm):
|
|
110
|
+
object_id = key.split(":")[1]
|
|
111
|
+
if regexp.match(object_id):
|
|
112
|
+
candidates.append((object_id, perm, value))
|
|
113
|
+
|
|
114
|
+
perms_by_object_id = {}
|
|
115
|
+
for object_id, perm, value in candidates:
|
|
116
|
+
if len(principals & value) > 0:
|
|
117
|
+
perms_by_object_id.setdefault(object_id, set()).add(perm)
|
|
118
|
+
return perms_by_object_id
|
|
119
|
+
|
|
120
|
+
@synchronized
|
|
121
|
+
def get_authorized_principals(self, bound_permissions):
|
|
122
|
+
principals = set()
|
|
123
|
+
for obj_id, perm in bound_permissions:
|
|
124
|
+
principals |= self.get_object_permission_principals(obj_id, perm)
|
|
125
|
+
return principals
|
|
126
|
+
|
|
127
|
+
@synchronized
|
|
128
|
+
def get_objects_permissions(self, objects_ids, permissions=None):
|
|
129
|
+
result = []
|
|
130
|
+
for object_id in objects_ids:
|
|
131
|
+
if permissions is None:
|
|
132
|
+
aces = [k for k in self._store.keys() if k.startswith(f"permission:{object_id}:")]
|
|
133
|
+
else:
|
|
134
|
+
aces = [f"permission:{object_id}:{permission}" for permission in permissions]
|
|
135
|
+
perms = {}
|
|
136
|
+
for ace in aces:
|
|
137
|
+
# Should work with 'permission:/url/id:object:create'.
|
|
138
|
+
permission = ace.split(":", 2)[2]
|
|
139
|
+
perms[permission] = set(self._store[ace])
|
|
140
|
+
result.append(perms)
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
@synchronized
|
|
144
|
+
def replace_object_permissions(self, object_id, permissions):
|
|
145
|
+
for permission, principals in permissions.items():
|
|
146
|
+
permission_key = f"permission:{object_id}:{permission}"
|
|
147
|
+
if permission_key in self._store and len(principals) == 0:
|
|
148
|
+
del self._store[permission_key]
|
|
149
|
+
elif principals:
|
|
150
|
+
self._store[permission_key] = set(principals)
|
|
151
|
+
return permissions
|
|
152
|
+
|
|
153
|
+
@synchronized
|
|
154
|
+
def delete_object_permissions(self, *object_id_list):
|
|
155
|
+
to_delete = []
|
|
156
|
+
for key in self._store.keys():
|
|
157
|
+
object_id = key.split(":")[1]
|
|
158
|
+
for pattern in object_id_list:
|
|
159
|
+
regexp = re.compile(f"^{pattern.replace('*', '.*')}$")
|
|
160
|
+
if regexp.match(object_id):
|
|
161
|
+
to_delete.append(key)
|
|
162
|
+
for k in to_delete:
|
|
163
|
+
del self._store[k]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def load_from_config(config):
|
|
167
|
+
return Permission()
|