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.
Files changed (142) hide show
  1. kinto/__init__.py +92 -0
  2. kinto/__main__.py +249 -0
  3. kinto/authorization.py +134 -0
  4. kinto/config/__init__.py +94 -0
  5. kinto/config/kinto.tpl +270 -0
  6. kinto/contribute.json +27 -0
  7. kinto/core/__init__.py +246 -0
  8. kinto/core/authentication.py +48 -0
  9. kinto/core/authorization.py +311 -0
  10. kinto/core/cache/__init__.py +131 -0
  11. kinto/core/cache/memcached.py +112 -0
  12. kinto/core/cache/memory.py +104 -0
  13. kinto/core/cache/postgresql/__init__.py +178 -0
  14. kinto/core/cache/postgresql/schema.sql +23 -0
  15. kinto/core/cache/testing.py +208 -0
  16. kinto/core/cornice/__init__.py +93 -0
  17. kinto/core/cornice/cors.py +144 -0
  18. kinto/core/cornice/errors.py +40 -0
  19. kinto/core/cornice/pyramidhook.py +373 -0
  20. kinto/core/cornice/renderer.py +89 -0
  21. kinto/core/cornice/resource.py +205 -0
  22. kinto/core/cornice/service.py +641 -0
  23. kinto/core/cornice/util.py +138 -0
  24. kinto/core/cornice/validators/__init__.py +94 -0
  25. kinto/core/cornice/validators/_colander.py +142 -0
  26. kinto/core/cornice/validators/_marshmallow.py +182 -0
  27. kinto/core/cornice_swagger/__init__.py +92 -0
  28. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  29. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  30. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  31. kinto/core/cornice_swagger/converters/schema.py +249 -0
  32. kinto/core/cornice_swagger/swagger.py +725 -0
  33. kinto/core/cornice_swagger/templates/index.html +73 -0
  34. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  35. kinto/core/cornice_swagger/util.py +42 -0
  36. kinto/core/cornice_swagger/views.py +78 -0
  37. kinto/core/decorators.py +74 -0
  38. kinto/core/errors.py +216 -0
  39. kinto/core/events.py +301 -0
  40. kinto/core/initialization.py +738 -0
  41. kinto/core/listeners/__init__.py +9 -0
  42. kinto/core/metrics.py +94 -0
  43. kinto/core/openapi.py +115 -0
  44. kinto/core/permission/__init__.py +202 -0
  45. kinto/core/permission/memory.py +167 -0
  46. kinto/core/permission/postgresql/__init__.py +489 -0
  47. kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
  48. kinto/core/permission/postgresql/schema.sql +41 -0
  49. kinto/core/permission/testing.py +487 -0
  50. kinto/core/resource/__init__.py +1311 -0
  51. kinto/core/resource/model.py +412 -0
  52. kinto/core/resource/schema.py +502 -0
  53. kinto/core/resource/viewset.py +230 -0
  54. kinto/core/schema.py +119 -0
  55. kinto/core/scripts.py +50 -0
  56. kinto/core/statsd.py +1 -0
  57. kinto/core/storage/__init__.py +436 -0
  58. kinto/core/storage/exceptions.py +53 -0
  59. kinto/core/storage/generators.py +58 -0
  60. kinto/core/storage/memory.py +651 -0
  61. kinto/core/storage/postgresql/__init__.py +1131 -0
  62. kinto/core/storage/postgresql/client.py +120 -0
  63. kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
  64. kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
  65. kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
  66. kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
  67. kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
  68. kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
  69. kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
  70. kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
  71. kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
  72. kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
  73. kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
  74. kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
  75. kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
  76. kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
  77. kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
  78. kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
  79. kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
  80. kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
  81. kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
  82. kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
  83. kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
  84. kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
  85. kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
  86. kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
  87. kinto/core/storage/postgresql/migrator.py +98 -0
  88. kinto/core/storage/postgresql/pool.py +55 -0
  89. kinto/core/storage/postgresql/schema.sql +143 -0
  90. kinto/core/storage/testing.py +1857 -0
  91. kinto/core/storage/utils.py +37 -0
  92. kinto/core/testing.py +182 -0
  93. kinto/core/utils.py +553 -0
  94. kinto/core/views/__init__.py +0 -0
  95. kinto/core/views/batch.py +163 -0
  96. kinto/core/views/errors.py +145 -0
  97. kinto/core/views/heartbeat.py +106 -0
  98. kinto/core/views/hello.py +69 -0
  99. kinto/core/views/openapi.py +35 -0
  100. kinto/core/views/version.py +50 -0
  101. kinto/events.py +3 -0
  102. kinto/plugins/__init__.py +0 -0
  103. kinto/plugins/accounts/__init__.py +94 -0
  104. kinto/plugins/accounts/authentication.py +63 -0
  105. kinto/plugins/accounts/scripts.py +61 -0
  106. kinto/plugins/accounts/utils.py +13 -0
  107. kinto/plugins/accounts/views.py +136 -0
  108. kinto/plugins/admin/README.md +3 -0
  109. kinto/plugins/admin/VERSION +1 -0
  110. kinto/plugins/admin/__init__.py +40 -0
  111. kinto/plugins/admin/build/VERSION +1 -0
  112. kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
  113. kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
  114. kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
  115. kinto/plugins/admin/build/index.html +18 -0
  116. kinto/plugins/admin/public/help.html +25 -0
  117. kinto/plugins/admin/views.py +42 -0
  118. kinto/plugins/default_bucket/__init__.py +191 -0
  119. kinto/plugins/flush.py +28 -0
  120. kinto/plugins/history/__init__.py +65 -0
  121. kinto/plugins/history/listener.py +181 -0
  122. kinto/plugins/history/views.py +66 -0
  123. kinto/plugins/openid/__init__.py +131 -0
  124. kinto/plugins/openid/utils.py +14 -0
  125. kinto/plugins/openid/views.py +193 -0
  126. kinto/plugins/prometheus.py +300 -0
  127. kinto/plugins/statsd.py +85 -0
  128. kinto/schema_validation.py +135 -0
  129. kinto/views/__init__.py +34 -0
  130. kinto/views/admin.py +195 -0
  131. kinto/views/buckets.py +45 -0
  132. kinto/views/collections.py +58 -0
  133. kinto/views/contribute.py +39 -0
  134. kinto/views/groups.py +90 -0
  135. kinto/views/permissions.py +235 -0
  136. kinto/views/records.py +133 -0
  137. kinto-23.2.1.dist-info/METADATA +232 -0
  138. kinto-23.2.1.dist-info/RECORD +142 -0
  139. kinto-23.2.1.dist-info/WHEEL +5 -0
  140. kinto-23.2.1.dist-info/entry_points.txt +5 -0
  141. kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
  142. kinto-23.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ class ListenerBase:
2
+ def __init__(self, *args, **kwargs):
3
+ pass
4
+
5
+ def __call__(self, event):
6
+ """
7
+ :param event: Incoming event
8
+ """
9
+ raise NotImplementedError()
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()