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,163 @@
1
+ import logging
2
+
3
+ import colander
4
+ from pyramid import httpexceptions
5
+ from pyramid.security import NO_PERMISSION_REQUIRED
6
+
7
+ from kinto.core import Service, errors
8
+ from kinto.core.cornice.validators import colander_validator
9
+ from kinto.core.errors import ErrorSchema
10
+ from kinto.core.resource.viewset import CONTENT_TYPES
11
+ from kinto.core.utils import build_request, build_response, merge_dicts
12
+
13
+
14
+ subrequest_logger = logging.getLogger("subrequest.summary")
15
+
16
+ valid_http_method = colander.OneOf(("GET", "HEAD", "DELETE", "TRACE", "POST", "PUT", "PATCH"))
17
+
18
+
19
+ def string_values(node, cstruct):
20
+ """Validate that a ``colander.Mapping`` only has strings in its values.
21
+
22
+ .. warning::
23
+
24
+ Should be associated to a ``colander.Mapping`` schema node.
25
+ """
26
+ are_strings = [isinstance(v, str) for v in cstruct.values()]
27
+ if not all(are_strings):
28
+ error_msg = f"{cstruct} contains non string value"
29
+ raise colander.Invalid(node, error_msg)
30
+
31
+
32
+ class BatchRequestSchema(colander.MappingSchema):
33
+ method = colander.SchemaNode(
34
+ colander.String(), validator=valid_http_method, missing=colander.drop
35
+ )
36
+ path = colander.SchemaNode(colander.String(), validator=colander.Regex("^/"))
37
+ headers = colander.SchemaNode(
38
+ colander.Mapping(unknown="preserve"), validator=string_values, missing=colander.drop
39
+ )
40
+ body = colander.SchemaNode(colander.Mapping(unknown="preserve"), missing=colander.drop)
41
+
42
+ @staticmethod
43
+ def schema_type():
44
+ return colander.Mapping(unknown="raise")
45
+
46
+
47
+ class BatchPayloadSchema(colander.MappingSchema):
48
+ defaults = BatchRequestSchema(missing=colander.drop).clone()
49
+ requests = colander.SchemaNode(colander.Sequence(), BatchRequestSchema())
50
+
51
+ @staticmethod
52
+ def schema_type():
53
+ return colander.Mapping(unknown="raise")
54
+
55
+ def __init__(self, *args, **kwargs):
56
+ super().__init__(*args, **kwargs)
57
+ # On defaults, path is not mandatory.
58
+ self.get("defaults").get("path").missing = colander.drop
59
+
60
+ def deserialize(self, cstruct=colander.null):
61
+ """Preprocess received data to carefully merge defaults."""
62
+ if cstruct is not colander.null:
63
+ defaults = cstruct.get("defaults")
64
+ requests = cstruct.get("requests")
65
+ if isinstance(defaults, dict) and isinstance(requests, list):
66
+ for request in requests:
67
+ if isinstance(request, dict):
68
+ merge_dicts(request, defaults)
69
+ return super().deserialize(cstruct)
70
+
71
+
72
+ class BatchRequest(colander.MappingSchema):
73
+ body = BatchPayloadSchema()
74
+
75
+
76
+ class BatchResponseSchema(colander.MappingSchema):
77
+ status = colander.SchemaNode(colander.Integer())
78
+ path = colander.SchemaNode(colander.String())
79
+ headers = colander.SchemaNode(
80
+ colander.Mapping(unknown="preserve"), validator=string_values, missing=colander.drop
81
+ )
82
+ body = colander.SchemaNode(colander.Mapping(unknown="preserve"), missing=colander.drop)
83
+
84
+
85
+ class BatchResponseBodySchema(colander.MappingSchema):
86
+ responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))
87
+
88
+
89
+ class BatchResponse(colander.MappingSchema):
90
+ body = BatchResponseBodySchema()
91
+
92
+
93
+ class ErrorResponseSchema(colander.MappingSchema):
94
+ body = ErrorSchema()
95
+
96
+
97
+ batch_responses = {
98
+ "200": BatchResponse(description="Return a list of operation responses."),
99
+ "400": ErrorResponseSchema(description="The request was badly formatted."),
100
+ "default": ErrorResponseSchema(description="an unknown error occurred."),
101
+ }
102
+
103
+ batch = Service(name="batch", path="/batch", description="Batch operations")
104
+
105
+
106
+ @batch.post(
107
+ schema=BatchRequest(),
108
+ validators=(colander_validator,),
109
+ content_type=CONTENT_TYPES,
110
+ permission=NO_PERMISSION_REQUIRED,
111
+ tags=["Batch"],
112
+ operation_id="batch",
113
+ response_schemas=batch_responses,
114
+ )
115
+ def post_batch(request):
116
+ requests = request.validated["body"]["requests"]
117
+
118
+ request.log_context(batch_size=len(requests))
119
+
120
+ limit = request.registry.settings["batch_max_requests"]
121
+ if limit and len(requests) > int(limit):
122
+ error_msg = f"Number of requests is limited to {limit}"
123
+ request.errors.add("body", "requests", error_msg)
124
+ return
125
+
126
+ if any([batch.path in req["path"] for req in requests]):
127
+ error_msg = f"Recursive call on {batch.path} endpoint is forbidden."
128
+ request.errors.add("body", "requests", error_msg)
129
+ return
130
+
131
+ responses = []
132
+
133
+ for subrequest_spec in requests:
134
+ subrequest = build_request(request, subrequest_spec)
135
+
136
+ log_context = {
137
+ **request.log_context(),
138
+ "path": subrequest.path,
139
+ "method": subrequest.method,
140
+ }
141
+ try:
142
+ # Invoke subrequest without individual transaction.
143
+ resp, subrequest = request.follow_subrequest(subrequest, use_tweens=False)
144
+ except httpexceptions.HTTPException as e:
145
+ # Since some request in the batch failed, we need to stop the parent request
146
+ # through Pyramid's transaction manager. 5XX errors are already caught by
147
+ # pyramid_tm's commit_veto
148
+ # https://github.com/Kinto/kinto/issues/624
149
+ if e.status_code == 409:
150
+ request.tm.abort()
151
+
152
+ if e.content_type == "application/json":
153
+ resp = e
154
+ else:
155
+ # JSONify raw Pyramid errors.
156
+ resp = errors.http_error(e)
157
+
158
+ subrequest_logger.info("subrequest.summary", extra=log_context)
159
+
160
+ dict_resp = build_response(resp, subrequest)
161
+ responses.append(dict_resp)
162
+
163
+ return {"responses": responses}
@@ -0,0 +1,145 @@
1
+ import logging
2
+
3
+ from pyramid import httpexceptions
4
+ from pyramid.authorization import Authenticated
5
+ from pyramid.httpexceptions import HTTPTemporaryRedirect
6
+ from pyramid.security import NO_PERMISSION_REQUIRED, forget
7
+ from pyramid.settings import asbool
8
+ from pyramid.view import view_config
9
+
10
+ from kinto.core.errors import ERRORS, http_error, request_GET
11
+ from kinto.core.storage import exceptions as storage_exceptions
12
+ from kinto.core.utils import reapply_cors
13
+
14
+
15
+ logger = logging.getLogger()
16
+
17
+
18
+ @view_config(context=httpexceptions.HTTPForbidden, permission=NO_PERMISSION_REQUIRED)
19
+ def authorization_required(response, request):
20
+ """Distinguish authentication required (``401 Unauthorized``) from
21
+ not allowed (``403 Forbidden``).
22
+ """
23
+ if Authenticated not in request.effective_principals:
24
+ if response.content_type != "application/json":
25
+ # This is always the case when `HTTPForbidden` is raised by Pyramid
26
+ # on protected views with unauthenticated requests.
27
+ error_msg = "Please authenticate yourself to use this endpoint."
28
+ response = http_error(
29
+ httpexceptions.HTTPUnauthorized(),
30
+ errno=ERRORS.MISSING_AUTH_TOKEN,
31
+ message=error_msg,
32
+ )
33
+ response.headers.extend(forget(request))
34
+ return response
35
+
36
+ if response.content_type != "application/json":
37
+ error_msg = "This user cannot access this resource."
38
+ response = http_error(
39
+ httpexceptions.HTTPForbidden(), errno=ERRORS.FORBIDDEN, message=error_msg
40
+ )
41
+ return reapply_cors(request, response)
42
+
43
+
44
+ @view_config(context=httpexceptions.HTTPNotFound, permission=NO_PERMISSION_REQUIRED)
45
+ def page_not_found(response, request):
46
+ """Return a JSON 404 error response."""
47
+ config_key = "trailing_slash_redirect_enabled"
48
+ redirect_enabled = request.registry.settings[config_key]
49
+ trailing_slash_redirection_enabled = asbool(redirect_enabled)
50
+
51
+ querystring = request.url[(request.url.rindex(request.path) + len(request.path)) :]
52
+
53
+ errno = ERRORS.MISSING_RESOURCE
54
+ error_msg = "The resource you are looking for could not be found."
55
+
56
+ if not request.path.startswith(f"/{request.registry.route_prefix}"):
57
+ errno = ERRORS.VERSION_NOT_AVAILABLE
58
+ error_msg = "The requested API version is not available on this server."
59
+ elif trailing_slash_redirection_enabled:
60
+ redirect = None
61
+
62
+ if request.path.endswith("/"):
63
+ path = request.path.rstrip("/")
64
+ redirect = f"{path}{querystring}"
65
+ elif request.path == f"/{request.registry.route_prefix}":
66
+ # Case for /v0 -> /v0/
67
+ redirect = f"/{request.registry.route_prefix}/{querystring}"
68
+
69
+ if redirect:
70
+ resp = HTTPTemporaryRedirect(redirect)
71
+ cache_seconds = int(request.registry.settings["trailing_slash_redirect_ttl_seconds"])
72
+ if cache_seconds >= 0:
73
+ resp.cache_expires(cache_seconds)
74
+ return reapply_cors(request, resp)
75
+
76
+ if response.content_type != "application/json":
77
+ response = http_error(httpexceptions.HTTPNotFound(), errno=errno, message=error_msg)
78
+ return reapply_cors(request, response)
79
+
80
+
81
+ @view_config(context=httpexceptions.HTTPServiceUnavailable, permission=NO_PERMISSION_REQUIRED)
82
+ def service_unavailable(response, request):
83
+ if response.content_type != "application/json":
84
+ error_msg = (
85
+ "Service temporary unavailable due to overloading or maintenance, please retry later."
86
+ )
87
+ response = http_error(response, errno=ERRORS.BACKEND, message=error_msg)
88
+
89
+ retry_after = request.registry.settings["retry_after_seconds"]
90
+ response.headers["Retry-After"] = str(retry_after)
91
+ return reapply_cors(request, response)
92
+
93
+
94
+ @view_config(context=httpexceptions.HTTPMethodNotAllowed, permission=NO_PERMISSION_REQUIRED)
95
+ def method_not_allowed(context, request):
96
+ if context.content_type == "application/json":
97
+ return context
98
+
99
+ response = http_error(
100
+ context, errno=ERRORS.METHOD_NOT_ALLOWED, message="Method not allowed on this endpoint."
101
+ )
102
+ return reapply_cors(request, response)
103
+
104
+
105
+ @view_config(context=Exception, permission=NO_PERMISSION_REQUIRED)
106
+ @view_config(context=httpexceptions.HTTPException, permission=NO_PERMISSION_REQUIRED)
107
+ def error(context, request):
108
+ """Catch server errors and trace them."""
109
+ if isinstance(context, httpexceptions.Response):
110
+ return reapply_cors(request, context)
111
+
112
+ if isinstance(context, storage_exceptions.IntegrityError):
113
+ error_msg = "Integrity constraint violated, please retry."
114
+ response = http_error(
115
+ httpexceptions.HTTPConflict(), errno=ERRORS.CONSTRAINT_VIOLATED, message=error_msg
116
+ )
117
+ retry_after = request.registry.settings["retry_after_seconds"]
118
+ response.headers["Retry-After"] = str(retry_after)
119
+ return reapply_cors(request, response)
120
+
121
+ # Log some information about current request.
122
+ extra = {"path": request.path, "method": request.method}
123
+ qs = dict(request_GET(request))
124
+ if qs:
125
+ extra["querystring"] = qs
126
+ # Take errno from original exception, or undefined if unknown/unhandled.
127
+ try:
128
+ extra["errno"] = context.errno.value
129
+ except AttributeError:
130
+ extra["errno"] = ERRORS.UNDEFINED.value
131
+
132
+ if isinstance(context, storage_exceptions.BackendError):
133
+ logger.critical(context.original, extra=extra, exc_info=context)
134
+ response = httpexceptions.HTTPServiceUnavailable()
135
+ return service_unavailable(response, request)
136
+
137
+ # Within the exception view, sys.exc_info() will return null.
138
+ # see https://github.com/python/cpython/blob/ce9e62544/Lib/logging/__init__.py#L1460-L1462
139
+ logger.error(context, extra=extra, exc_info=context)
140
+
141
+ error_msg = "A programmatic error occured, developers have been informed."
142
+ info = request.registry.settings["error_info_link"]
143
+ response = http_error(httpexceptions.HTTPInternalServerError(), message=error_msg, info=info)
144
+
145
+ return reapply_cors(request, response)
@@ -0,0 +1,106 @@
1
+ import logging
2
+ from concurrent.futures import ThreadPoolExecutor, wait
3
+
4
+ import colander
5
+ import transaction
6
+ from pyramid.security import NO_PERMISSION_REQUIRED
7
+
8
+ from kinto.core import Service
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ heartbeat = Service(name="heartbeat", path="/__heartbeat__", description="Server health")
15
+
16
+
17
+ class HeartbeatResponseSchema(colander.MappingSchema):
18
+ body = colander.SchemaNode(colander.Mapping(unknown="preserve"))
19
+
20
+
21
+ heartbeat_responses = {
22
+ "200": HeartbeatResponseSchema(description="Server is working properly."),
23
+ "503": HeartbeatResponseSchema(description="One or more subsystems failing."),
24
+ }
25
+
26
+
27
+ @heartbeat.get(
28
+ permission=NO_PERMISSION_REQUIRED,
29
+ tags=["Utilities"],
30
+ operation_id="__heartbeat__",
31
+ response_schemas=heartbeat_responses,
32
+ )
33
+ def get_heartbeat(request):
34
+ """Return information about server health."""
35
+ status = {}
36
+
37
+ def heartbeat_check(name, func):
38
+ status[name] = False
39
+ status[name] = func(request)
40
+ # Since the heartbeat checks run concurrently, their transactions
41
+ # overlap and might end in shared lock errors. By aborting here
42
+ # we clean-up the state on each heartbeat call instead of once at the
43
+ # end of the request. See bug Kinto/kinto#804
44
+ transaction.abort()
45
+
46
+ # Start executing heartbeats concurrently.
47
+ heartbeats = request.registry.heartbeats
48
+ pool = ThreadPoolExecutor(max_workers=max(1, len(heartbeats.keys())))
49
+ futures = []
50
+ for name, func in heartbeats.items():
51
+ future = pool.submit(heartbeat_check, name, func)
52
+ future.__heartbeat_name = name # For logging purposes.
53
+ futures.append(future)
54
+
55
+ # Wait for the results, with timeout.
56
+ seconds = float(request.registry.settings["heartbeat_timeout_seconds"])
57
+ done, not_done = wait(futures, timeout=seconds)
58
+
59
+ # A heartbeat is supposed to return True or False, and never raise.
60
+ # Just in case, go though results to spot any potential exception.
61
+ for future in done:
62
+ exc = future.exception()
63
+ if exc is not None:
64
+ logger.error(f"'{future.__heartbeat_name}' heartbeat failed.")
65
+ logger.error(exc)
66
+
67
+ # Log timed-out heartbeats.
68
+ for future in not_done:
69
+ name = future.__heartbeat_name
70
+ error_msg = f"'{name}' heartbeat has exceeded timeout of {seconds} seconds."
71
+ logger.error(error_msg)
72
+
73
+ # If any has failed, return a 503 error response.
74
+ has_error = not all([v or v is None for v in status.values()])
75
+ if has_error:
76
+ request.response.status = 503
77
+
78
+ return status
79
+
80
+
81
+ class LbHeartbeatResponseSchema(colander.MappingSchema):
82
+ body = colander.SchemaNode(colander.Mapping())
83
+
84
+
85
+ lbheartbeat_responses = {
86
+ "200": LbHeartbeatResponseSchema(description="Returned if server is reachable.")
87
+ }
88
+
89
+
90
+ lbheartbeat = Service(name="lbheartbeat", path="/__lbheartbeat__", description="Web head health")
91
+
92
+
93
+ @lbheartbeat.get(
94
+ permission=NO_PERMISSION_REQUIRED,
95
+ tags=["Utilities"],
96
+ operation_id="__lbheartbeat__",
97
+ response_schemas=lbheartbeat_responses,
98
+ )
99
+ def get_lbheartbeat(request):
100
+ """Return successful healthy response.
101
+
102
+ If the load-balancer tries to access this URL and fails, this means the
103
+ Web head is not operational and should be dropped.
104
+ """
105
+ status = {}
106
+ return status
@@ -0,0 +1,69 @@
1
+ import colander
2
+ from pyramid.authorization import Authenticated
3
+ from pyramid.security import NO_PERMISSION_REQUIRED
4
+
5
+ from kinto.config import config_attributes
6
+ from kinto.core import Service
7
+
8
+
9
+ hello = Service(name="hello", path="/", description="Welcome")
10
+
11
+
12
+ class HelloResponseSchema(colander.MappingSchema):
13
+ body = colander.SchemaNode(colander.Mapping(unknown="preserve"))
14
+
15
+
16
+ hello_response_schemas = {
17
+ "200": HelloResponseSchema(description="Return information about the running Instance.")
18
+ }
19
+
20
+
21
+ @hello.get(
22
+ permission=NO_PERMISSION_REQUIRED,
23
+ tags=["Utilities"],
24
+ operation_id="server_info",
25
+ response_schemas=hello_response_schemas,
26
+ )
27
+ def get_hello(request):
28
+ """Return information regarding the current instance."""
29
+ settings = request.registry.settings
30
+
31
+ project_name = settings["project_name"]
32
+ project_version = settings["project_version"]
33
+
34
+ data = dict(
35
+ project_name=project_name,
36
+ project_version=project_version,
37
+ http_api_version=settings["http_api_version"],
38
+ project_docs=settings["project_docs"],
39
+ url=request.route_url(hello.name),
40
+ config=config_attributes(),
41
+ )
42
+
43
+ eos = get_eos(request)
44
+ if eos:
45
+ data["eos"] = eos
46
+
47
+ data["settings"] = {}
48
+ public_settings = request.registry.public_settings
49
+ for setting in list(public_settings):
50
+ data["settings"][setting] = settings[setting]
51
+
52
+ # If current user is authenticated, add user info:
53
+ # (Note: this will call authenticated_userid() with multiauth+groupfinder)
54
+ if Authenticated in request.effective_principals:
55
+ data["user"] = request.get_user_info()
56
+
57
+ if settings["readonly"]:
58
+ # Information can be cached.
59
+ cache_seconds = int(settings["root_cache_expires_seconds"])
60
+ request.response.cache_expires(cache_seconds)
61
+
62
+ # Application can register and expose arbitrary capabilities.
63
+ data["capabilities"] = request.registry.api_capabilities
64
+
65
+ return data
66
+
67
+
68
+ def get_eos(request):
69
+ return request.registry.settings["eos"]
@@ -0,0 +1,35 @@
1
+ import colander
2
+ from pyramid.security import NO_PERMISSION_REQUIRED
3
+
4
+ from kinto.core import Service
5
+ from kinto.core.cornice.service import get_services
6
+ from kinto.core.openapi import OpenAPI
7
+
8
+
9
+ openapi = Service(name="openapi", path="/__api__", description="OpenAPI description")
10
+
11
+
12
+ class OpenAPIResponseSchema(colander.MappingSchema):
13
+ body = colander.SchemaNode(colander.Mapping(unknown="preserve"))
14
+
15
+
16
+ openapi_response_schemas = {
17
+ "200": OpenAPIResponseSchema(
18
+ description="Return an OpenAPI description of the running instance."
19
+ )
20
+ }
21
+
22
+
23
+ @openapi.get(
24
+ permission=NO_PERMISSION_REQUIRED,
25
+ response_schemas=openapi_response_schemas,
26
+ tags=["Utilities"],
27
+ operation_id="get_openapi_spec",
28
+ )
29
+ def openapi_view(request):
30
+ # Only build json once
31
+ try:
32
+ return openapi_view.__json__
33
+ except AttributeError:
34
+ openapi_view.__json__ = OpenAPI(get_services(), request).generate()
35
+ return openapi_view.__json__
@@ -0,0 +1,50 @@
1
+ import os
2
+
3
+ import colander
4
+ from pyramid.security import NO_PERMISSION_REQUIRED
5
+
6
+ from kinto.core import Service
7
+ from kinto.core.utils import json
8
+
9
+
10
+ HERE = os.path.dirname(__file__)
11
+ ORIGIN = os.path.dirname(HERE)
12
+
13
+
14
+ class VersionResponseSchema(colander.MappingSchema):
15
+ body = colander.SchemaNode(colander.Mapping(unknown="preserve"))
16
+
17
+
18
+ version_response_schemas = {
19
+ "200": VersionResponseSchema(description="Return the running Instance version information.")
20
+ }
21
+
22
+
23
+ version = Service(name="version", path="/__version__", description="Version")
24
+
25
+
26
+ @version.get(
27
+ permission=NO_PERMISSION_REQUIRED,
28
+ tags=["Utilities"],
29
+ operation_id="__version__",
30
+ response_schemas=version_response_schemas,
31
+ )
32
+ def version_view(request):
33
+ try:
34
+ return version_view.__json__
35
+ except AttributeError:
36
+ pass
37
+
38
+ location = request.registry.settings["version_json_path"]
39
+ files = [
40
+ location, # Default is current working dir.
41
+ os.path.join(ORIGIN, "version.json"), # Relative to the package root.
42
+ os.path.join(HERE, "version.json"), # Relative to this file.
43
+ ]
44
+ for version_file in files:
45
+ if os.path.exists(version_file):
46
+ with open(version_file) as f:
47
+ version_view.__json__ = json.load(f)
48
+ return version_view.__json__ # First one wins.
49
+
50
+ raise FileNotFoundError("Version file missing from {}".format(",".join(files)))
kinto/events.py ADDED
@@ -0,0 +1,3 @@
1
+ class ServerFlushed:
2
+ def __init__(self, request):
3
+ self.request = request
File without changes
@@ -0,0 +1,94 @@
1
+ import re
2
+ import sys
3
+
4
+ from pyramid.exceptions import ConfigurationError
5
+
6
+ from kinto.authorization import PERMISSIONS_INHERITANCE_TREE
7
+
8
+ from .authentication import AccountsAuthenticationPolicy as AccountsPolicy
9
+ from .utils import ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME
10
+
11
+
12
+ __all__ = [
13
+ "ACCOUNT_CACHE_KEY",
14
+ "ACCOUNT_POLICY_NAME",
15
+ "AccountsPolicy",
16
+ ]
17
+
18
+ DOCS_URL = "https://kinto.readthedocs.io/en/stable/api/1.x/accounts.html"
19
+
20
+
21
+ def includeme(config):
22
+ settings = config.get_settings()
23
+ config.add_api_capability(
24
+ "accounts",
25
+ description="Manage user accounts.",
26
+ url="https://kinto.readthedocs.io/en/latest/api/1.x/accounts.html",
27
+ validation_enabled=False,
28
+ )
29
+ kwargs = {}
30
+ config.scan("kinto.plugins.accounts.views", **kwargs)
31
+
32
+ PERMISSIONS_INHERITANCE_TREE["root"].update({"account:create": {}})
33
+ PERMISSIONS_INHERITANCE_TREE["account"] = {
34
+ "write": {"account": ["write"]},
35
+ "read": {"account": ["write", "read"]},
36
+ }
37
+
38
+ # Check that the account policy is mentioned in config if included.
39
+ accountClass = "AccountsPolicy"
40
+ policy = None
41
+ for k, v in settings.items():
42
+ m = re.match("multiauth\\.policy\\.(.*)\\.use", k)
43
+ if m:
44
+ if v.endswith(accountClass) or v.endswith("AccountsAuthenticationPolicy"):
45
+ policy = m.group(1)
46
+
47
+ if settings["storage_backend"] == "kinto.core.storage.memory": # pragma: no cover
48
+ error_msg = (
49
+ "\033[1;31;40m"
50
+ "The account plugin works really poorly with the memory backend because "
51
+ "accounts are flushed at each startup.\nThis is why you can't use the "
52
+ "kinto create-user command with it."
53
+ "\033[0;37;40m"
54
+ )
55
+ print(error_msg, file=sys.stderr)
56
+
57
+ if not policy:
58
+ error_msg = (
59
+ "Account policy missing the 'multiauth.policy.*.use' "
60
+ f"setting. See {accountClass} in docs {DOCS_URL}."
61
+ )
62
+ raise ConfigurationError(error_msg)
63
+
64
+ # Add some safety to avoid weird behaviour with basicauth default policy.
65
+ auth_policies = settings["multiauth.policies"]
66
+ if "basicauth" in auth_policies and policy in auth_policies:
67
+ if auth_policies.index("basicauth") < auth_policies.index(policy):
68
+ error_msg = (
69
+ "'basicauth' should not be mentioned before '%s' in 'multiauth.policies' setting."
70
+ ) % policy
71
+ raise ConfigurationError(error_msg)
72
+
73
+ # We assume anyone in account_create_principals is to create
74
+ # accounts for other people.
75
+ # No one can create accounts for other people unless they are an
76
+ # "admin", defined as someone matching account_write_principals.
77
+ # Therefore any account that is in account_create_principals
78
+ # should be in account_write_principals too.
79
+ creators = set(settings.get("account_create_principals", "").split())
80
+ admins = set(settings.get("account_write_principals", "").split())
81
+ cant_create_anything = creators.difference(admins)
82
+ # system.Everyone isn't an account.
83
+ cant_create_anything.discard("system.Everyone")
84
+ if cant_create_anything:
85
+ message = (
86
+ "Configuration has some principals in account_create_principals "
87
+ "but not in account_write_principals. These principals will only be "
88
+ "able to create their own accounts. This may not be what you want.\n"
89
+ "If you want these users to be able to create accounts for other users, "
90
+ "add them to account_write_principals.\n"
91
+ f"Affected users: {list(cant_create_anything)}"
92
+ )
93
+
94
+ raise ConfigurationError(message)