kinto 19.5.0__py3-none-any.whl → 20.0.0__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.

Potentially problematic release.


This version of kinto might be problematic. Click here for more details.

Files changed (70) hide show
  1. kinto/__main__.py +0 -17
  2. kinto/config/kinto.tpl +0 -13
  3. kinto/contribute.json +27 -0
  4. kinto/core/__init__.py +3 -3
  5. kinto/core/cornice/__init__.py +93 -0
  6. kinto/core/cornice/cors.py +144 -0
  7. kinto/core/cornice/errors.py +40 -0
  8. kinto/core/cornice/pyramidhook.py +373 -0
  9. kinto/core/cornice/renderer.py +89 -0
  10. kinto/core/cornice/resource.py +205 -0
  11. kinto/core/cornice/service.py +641 -0
  12. kinto/core/cornice/util.py +138 -0
  13. kinto/core/cornice/validators/__init__.py +94 -0
  14. kinto/core/cornice/validators/_colander.py +142 -0
  15. kinto/core/cornice/validators/_marshmallow.py +182 -0
  16. kinto/core/cornice_swagger/__init__.py +92 -0
  17. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  18. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  19. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  20. kinto/core/cornice_swagger/converters/schema.py +249 -0
  21. kinto/core/cornice_swagger/swagger.py +725 -0
  22. kinto/core/cornice_swagger/templates/index.html +73 -0
  23. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  24. kinto/core/cornice_swagger/util.py +42 -0
  25. kinto/core/cornice_swagger/views.py +78 -0
  26. kinto/core/initialization.py +0 -14
  27. kinto/core/openapi.py +2 -3
  28. kinto/core/resource/viewset.py +1 -1
  29. kinto/core/storage/postgresql/pool.py +1 -1
  30. kinto/core/testing.py +1 -1
  31. kinto/core/utils.py +3 -2
  32. kinto/core/views/batch.py +1 -1
  33. kinto/core/views/errors.py +2 -0
  34. kinto/core/views/openapi.py +1 -1
  35. kinto/plugins/accounts/__init__.py +2 -19
  36. kinto/plugins/accounts/authentication.py +8 -54
  37. kinto/plugins/accounts/utils.py +0 -133
  38. kinto/plugins/accounts/{views/__init__.py → views.py} +7 -62
  39. kinto/plugins/admin/VERSION +1 -1
  40. kinto/plugins/admin/build/VERSION +1 -1
  41. kinto/plugins/admin/build/assets/asn1-EdZsLKOL.js +1 -0
  42. kinto/plugins/admin/build/assets/index-Bq62Gei8.js +165 -0
  43. kinto/plugins/admin/build/assets/{index-BdpYyatM.css → index-Cs7JVwIg.css} +1 -1
  44. kinto/plugins/admin/build/assets/javascript-qCveANmP.js +1 -0
  45. kinto/plugins/admin/build/assets/mllike-CXdrOF99.js +1 -0
  46. kinto/plugins/admin/build/assets/sql-D0XecflT.js +1 -0
  47. kinto/plugins/admin/build/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  48. kinto/plugins/admin/build/index.html +2 -2
  49. kinto/plugins/flush.py +1 -1
  50. kinto/plugins/openid/views.py +1 -1
  51. kinto/views/contribute.py +14 -13
  52. {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/METADATA +2 -6
  53. {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/RECORD +57 -42
  54. {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/WHEEL +1 -1
  55. kinto/plugins/accounts/mails.py +0 -96
  56. kinto/plugins/accounts/views/validation.py +0 -136
  57. kinto/plugins/admin/build/assets/asn1-CGOzndHr.js +0 -1
  58. kinto/plugins/admin/build/assets/index-n-QM_iZE.js +0 -165
  59. kinto/plugins/admin/build/assets/javascript-iSgyE4tI.js +0 -1
  60. kinto/plugins/admin/build/assets/mllike-C_8OmSiT.js +0 -1
  61. kinto/plugins/admin/build/assets/sql-C4g8LzGK.js +0 -1
  62. kinto/plugins/admin/build/assets/ttcn-cfg-BIkV9KBc.js +0 -1
  63. kinto/plugins/quotas/__init__.py +0 -21
  64. kinto/plugins/quotas/listener.py +0 -226
  65. kinto/plugins/quotas/scripts.py +0 -80
  66. kinto/plugins/quotas/utils.py +0 -7
  67. kinto/scripts.py +0 -41
  68. {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/LICENSE +0 -0
  69. {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/entry_points.txt +0 -0
  70. {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,73 @@
1
+ <!-- HTML for static distribution bundle build -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>Swagger UI</title>
7
+ <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
8
+ <link rel="stylesheet" type="text/css" href="${ui_css_url}" >
9
+ <style>
10
+ html
11
+ {
12
+ box-sizing: border-box;
13
+ overflow: -moz-scrollbars-vertical;
14
+ overflow-y: scroll;
15
+ }
16
+ *,
17
+ *:before,
18
+ *:after
19
+ {
20
+ box-sizing: inherit;
21
+ }
22
+
23
+ body {
24
+ margin:0;
25
+ background: #fafafa;
26
+ }
27
+ </style>
28
+ </head>
29
+
30
+ <body>
31
+
32
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
33
+ <defs>
34
+ <symbol viewBox="0 0 20 20" id="unlocked">
35
+ <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
36
+ </symbol>
37
+
38
+ <symbol viewBox="0 0 20 20" id="locked">
39
+ <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
40
+ </symbol>
41
+
42
+ <symbol viewBox="0 0 20 20" id="close">
43
+ <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
44
+ </symbol>
45
+
46
+ <symbol viewBox="0 0 20 20" id="large-arrow">
47
+ <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
48
+ </symbol>
49
+
50
+ <symbol viewBox="0 0 20 20" id="large-arrow-down">
51
+ <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
52
+ </symbol>
53
+
54
+
55
+ <symbol viewBox="0 0 24 24" id="jump-to">
56
+ <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
57
+ </symbol>
58
+
59
+ <symbol viewBox="0 0 24 24" id="expand">
60
+ <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
61
+ </symbol>
62
+
63
+ </defs>
64
+ </svg>
65
+
66
+ <div id="swagger-ui"></div>
67
+
68
+ <script src="${ui_js_bundle_url}"> </script>
69
+ <script src="${ui_js_standalone_url}"> </script>
70
+ ${swagger_ui_script}
71
+ </body>
72
+
73
+ </html>
@@ -0,0 +1,21 @@
1
+ <script>
2
+ window.onload = function() {
3
+
4
+ // Build a system
5
+ const ui = SwaggerUIBundle({
6
+ url: "${swagger_spec_url}",
7
+ dom_id: '#swagger-ui',
8
+ deepLinking: true,
9
+ presets: [
10
+ SwaggerUIBundle.presets.apis,
11
+ SwaggerUIStandalonePreset
12
+ ],
13
+ plugins: [
14
+ SwaggerUIBundle.plugins.DownloadUrl
15
+ ],
16
+ layout: "StandaloneLayout"
17
+ })
18
+
19
+ window.ui = ui
20
+ }
21
+ </script>
@@ -0,0 +1,42 @@
1
+ import colander
2
+
3
+ from kinto.core.cornice.validators import colander_body_validator
4
+
5
+
6
+ def trim(docstring):
7
+ """
8
+ Remove the tabs to spaces, and remove the extra spaces / tabs that are in
9
+ front of the text in docstrings.
10
+
11
+ Implementation taken from http://www.python.org/dev/peps/pep-0257/
12
+ """
13
+ if not docstring:
14
+ return ""
15
+ # Convert tabs to spaces (following the normal Python rules)
16
+ # and split into a list of lines:
17
+ lines = docstring.expandtabs().splitlines()
18
+ lines = [line.strip() for line in lines]
19
+ res = "\n".join(lines)
20
+ return res
21
+
22
+
23
+ def body_schema_transformer(schema, args):
24
+ validators = args.get("validators", [])
25
+ if colander_body_validator in validators:
26
+ body_schema = schema
27
+ schema = colander.MappingSchema()
28
+ schema["body"] = body_schema
29
+ return schema
30
+
31
+
32
+ def merge_dicts(base, changes):
33
+ """Merge b into a recursively, without overwriting values.
34
+
35
+ :param base: the dict that will be altered.
36
+ :param changes: changes to update base.
37
+ """
38
+ for k, v in changes.items():
39
+ if isinstance(v, dict):
40
+ merge_dicts(base.setdefault(k, {}), v)
41
+ else:
42
+ base.setdefault(k, v)
@@ -0,0 +1,78 @@
1
+ import importlib
2
+ from string import Template
3
+
4
+ import cornice
5
+ import cornice_swagger
6
+ import pkg_resources
7
+ from pyramid.response import Response
8
+
9
+
10
+ # hardcode for now since that will work for vast majority of users
11
+ # maybe later add minified resources for behind firewall support?
12
+ ui_css_url = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.11/swagger-ui.css"
13
+ ui_js_bundle_url = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.11/swagger-ui-bundle.js"
14
+ ui_js_standalone_url = (
15
+ "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.11/swagger-ui-standalone-preset.js"
16
+ )
17
+
18
+
19
+ def swagger_ui_template_view(request):
20
+ """
21
+ Serves Swagger UI page, default Swagger UI config is used but you can
22
+ override the callable that generates the `<script>` tag by setting
23
+ `cornice_swagger.swagger_ui_script_generator` in pyramid config, it defaults
24
+ to 'cornice_swagger.views:swagger_ui_script_template'
25
+
26
+ :param request:
27
+ :return:
28
+ """
29
+ script_generator = request.registry.settings.get(
30
+ "cornice_swagger.swagger_ui_script_generator",
31
+ "cornice_swagger.views:swagger_ui_script_template",
32
+ )
33
+ package, callable = script_generator.split(":")
34
+ imported_package = importlib.import_module(package)
35
+ script_callable = getattr(imported_package, callable)
36
+ template = pkg_resources.resource_string("cornice_swagger", "templates/index.html").decode(
37
+ "utf8"
38
+ )
39
+
40
+ html = Template(template).safe_substitute(
41
+ ui_css_url=ui_css_url,
42
+ ui_js_bundle_url=ui_js_bundle_url,
43
+ ui_js_standalone_url=ui_js_standalone_url,
44
+ swagger_ui_script=script_callable(request),
45
+ )
46
+ return Response(html)
47
+
48
+
49
+ def open_api_json_view(request):
50
+ """
51
+ :param request:
52
+ :return:
53
+
54
+ Generates JSON representation of Swagger spec
55
+ """
56
+ doc = cornice_swagger.CorniceSwagger(
57
+ cornice.service.get_services(), pyramid_registry=request.registry
58
+ )
59
+ kwargs = request.registry.settings["cornice_swagger.spec_kwargs"]
60
+ my_spec = doc.generate(**kwargs)
61
+ return my_spec
62
+
63
+
64
+ def swagger_ui_script_template(request, **kwargs):
65
+ """
66
+ :param request:
67
+ :return:
68
+
69
+ Generates the <script> code that bootstraps Swagger UI, it will be injected
70
+ into index template
71
+ """
72
+ swagger_spec_url = request.route_url("cornice_swagger.open_api_path")
73
+ template = pkg_resources.resource_string(
74
+ "cornice_swagger", "templates/index_script_template.html"
75
+ ).decode("utf8")
76
+ return Template(template).safe_substitute(
77
+ swagger_spec_url=swagger_spec_url,
78
+ )
@@ -334,16 +334,6 @@ def setup_sentry(config):
334
334
  config.add_subscriber(on_app_created, ApplicationCreated)
335
335
 
336
336
 
337
- def setup_statsd(config):
338
- # It would be pretty rare to find users that have a custom ``kinto.initialization_sequence`` setting.
339
- # But just in case, warn that it will be removed in next major.
340
- warnings.warn(
341
- "``setup_statsd()`` is now deprecated. Use ``kinto.core.initialization.setup_metrics()`` instead.",
342
- DeprecationWarning,
343
- )
344
- setup_metrics(config)
345
-
346
-
347
337
  def install_middlewares(app, settings):
348
338
  "Install a set of middlewares defined in the ini file on the given app."
349
339
  # Setup new-relic.
@@ -544,10 +534,6 @@ def setup_metrics(config):
544
534
 
545
535
  config.add_subscriber(on_new_response, NewResponse)
546
536
 
547
- # While statsd is deprecated, we include its plugin by default for retro-compability.
548
- if settings["statsd_url"]:
549
- config.include("kinto.plugins.statsd")
550
-
551
537
 
552
538
  class EventActionFilter:
553
539
  def __init__(self, actions, config):
kinto/core/openapi.py CHANGED
@@ -1,6 +1,5 @@
1
- from cornice_swagger import CorniceSwagger
2
- from cornice_swagger.converters.schema import TypeConverter
3
-
1
+ from kinto.core.cornice_swagger import CorniceSwagger
2
+ from kinto.core.cornice_swagger.converters.schema import TypeConverter
4
3
  from kinto.core.schema import Any
5
4
 
6
5
 
@@ -2,10 +2,10 @@ import functools
2
2
  import warnings
3
3
 
4
4
  import colander
5
- from cornice.validators import colander_validator
6
5
  from pyramid.settings import asbool
7
6
 
8
7
  from kinto.core import authorization
8
+ from kinto.core.cornice.validators import colander_validator
9
9
 
10
10
  from .schema import (
11
11
  ObjectGetQuerySchema,
@@ -21,7 +21,7 @@ class _QueueWithMaxBacklog(Queue):
21
21
  with self.mutex:
22
22
  self.cur_backlog += 1
23
23
  try:
24
- if self.max_backlog >= 0:
24
+ if self.max_backlog >= 0: # pragma: no branch
25
25
  if self.cur_backlog > self.max_backlog:
26
26
  block = False
27
27
  timeout = None
kinto/core/testing.py CHANGED
@@ -5,10 +5,10 @@ from collections import defaultdict
5
5
  from unittest import mock
6
6
 
7
7
  import webtest
8
- from cornice import errors as cornice_errors
9
8
  from pyramid.url import parse_url_overrides
10
9
 
11
10
  from kinto.core import DEFAULT_SETTINGS
11
+ from kinto.core.cornice import errors as cornice_errors
12
12
  from kinto.core.storage import generators
13
13
  from kinto.core.utils import encode64, follow_subrequest, memcache, sqlalchemy
14
14
  from kinto.plugins import prometheus, statsd
kinto/core/utils.py CHANGED
@@ -13,7 +13,6 @@ from urllib.parse import unquote
13
13
  import jsonpatch
14
14
  import rapidjson
15
15
  from colander import null
16
- from cornice import cors
17
16
  from pyramid import httpexceptions
18
17
  from pyramid.authorization import Authenticated
19
18
  from pyramid.interfaces import IRoutesMapper
@@ -21,6 +20,8 @@ from pyramid.request import Request, apply_request_extensions
21
20
  from pyramid.settings import aslist
22
21
  from pyramid.view import render_view_to_response
23
22
 
23
+ from kinto.core.cornice import cors
24
+
24
25
 
25
26
  try:
26
27
  import sqlalchemy
@@ -289,7 +290,7 @@ def current_service(request):
289
290
  """Return the Cornice service matching the specified request.
290
291
 
291
292
  :returns: the service or None if unmatched.
292
- :rtype: cornice.Service
293
+ :rtype: kinto.core.cornice.Service
293
294
  """
294
295
  if request.matched_route:
295
296
  services = request.registry.cornice_services
kinto/core/views/batch.py CHANGED
@@ -1,11 +1,11 @@
1
1
  import logging
2
2
 
3
3
  import colander
4
- from cornice.validators import colander_validator
5
4
  from pyramid import httpexceptions
6
5
  from pyramid.security import NO_PERMISSION_REQUIRED
7
6
 
8
7
  from kinto.core import Service, errors
8
+ from kinto.core.cornice.validators import colander_validator
9
9
  from kinto.core.errors import ErrorSchema
10
10
  from kinto.core.resource.viewset import CONTENT_TYPES
11
11
  from kinto.core.utils import build_request, build_response, merge_dicts
@@ -22,6 +22,8 @@ def authorization_required(response, request):
22
22
  """
23
23
  if Authenticated not in request.effective_principals:
24
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.
25
27
  error_msg = "Please authenticate yourself to use this endpoint."
26
28
  response = http_error(
27
29
  httpexceptions.HTTPUnauthorized(),
@@ -1,8 +1,8 @@
1
1
  import colander
2
- from cornice.service import get_services
3
2
  from pyramid.security import NO_PERMISSION_REQUIRED
4
3
 
5
4
  from kinto.core import Service
5
+ from kinto.core.cornice.service import get_services
6
6
  from kinto.core.openapi import OpenAPI
7
7
 
8
8
 
@@ -6,19 +6,12 @@ from pyramid.exceptions import ConfigurationError
6
6
  from kinto.authorization import PERMISSIONS_INHERITANCE_TREE
7
7
 
8
8
  from .authentication import AccountsAuthenticationPolicy as AccountsPolicy
9
- from .utils import (
10
- ACCOUNT_CACHE_KEY,
11
- ACCOUNT_POLICY_NAME,
12
- ACCOUNT_RESET_PASSWORD_CACHE_KEY,
13
- ACCOUNT_VALIDATION_CACHE_KEY,
14
- )
9
+ from .utils import ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME
15
10
 
16
11
 
17
12
  __all__ = [
18
13
  "ACCOUNT_CACHE_KEY",
19
14
  "ACCOUNT_POLICY_NAME",
20
- "ACCOUNT_RESET_PASSWORD_CACHE_KEY",
21
- "ACCOUNT_VALIDATION_CACHE_KEY",
22
15
  "AccountsPolicy",
23
16
  ]
24
17
 
@@ -27,16 +20,13 @@ DOCS_URL = "https://kinto.readthedocs.io/en/stable/api/1.x/accounts.html"
27
20
 
28
21
  def includeme(config):
29
22
  settings = config.get_settings()
30
- validation_enabled = settings.get("account_validation", False)
31
23
  config.add_api_capability(
32
24
  "accounts",
33
25
  description="Manage user accounts.",
34
26
  url="https://kinto.readthedocs.io/en/latest/api/1.x/accounts.html",
35
- validation_enabled=validation_enabled,
27
+ validation_enabled=False,
36
28
  )
37
29
  kwargs = {}
38
- if not validation_enabled:
39
- kwargs["ignore"] = "kinto.plugins.accounts.views.validation"
40
30
  config.scan("kinto.plugins.accounts.views", **kwargs)
41
31
 
42
32
  PERMISSIONS_INHERITANCE_TREE["root"].update({"account:create": {}})
@@ -45,13 +35,6 @@ def includeme(config):
45
35
  "read": {"account": ["write", "read"]},
46
36
  }
47
37
 
48
- if validation_enabled:
49
- # Valid mailers other than the default are `debug` and `testing`
50
- # according to
51
- # https://docs.pylonsproject.org/projects/pyramid_mailer/en/latest/#debugging
52
- mailer = settings.get("mail.mailer", "")
53
- config.include("pyramid_mailer" + (f".{mailer}" if mailer else ""))
54
-
55
38
  # Check that the account policy is mentioned in config if included.
56
39
  accountClass = "AccountsPolicy"
57
40
  policy = None
@@ -5,31 +5,27 @@ from kinto.core import utils
5
5
  from kinto.core.storage import exceptions as storage_exceptions
6
6
 
7
7
  from .utils import (
8
+ ACCOUNT_CACHE_KEY,
8
9
  ACCOUNT_POLICY_NAME,
9
- cache_account,
10
- delete_cached_reset_password,
11
- get_account_cache_key,
12
- get_cached_account,
13
- get_cached_reset_password,
14
- is_validated,
15
- refresh_cached_account,
16
10
  )
17
11
 
18
12
 
19
13
  def account_check(username, password, request):
20
14
  settings = request.registry.settings
21
- validation_enabled = settings.get("account_validation", False)
22
- cache_key = get_account_cache_key(username, request.registry)
15
+ hmac_secret = settings["userid_hmac_secret"]
16
+ cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username))
17
+ cache_ttl = int(settings.get("account_cache_ttl_seconds", 30))
23
18
  hashed_password = utils.hmac_digest(cache_key, password)
24
19
 
25
20
  # Check cache to see whether somebody has recently logged in with the same
26
21
  # username and password.
27
- cache_result = get_cached_account(username, request.registry)
22
+ cache = request.registry.cache
23
+ cache_result = cache.get(cache_key)
28
24
 
29
25
  # Username and password have been verified previously. No need to compare hashes
30
26
  if cache_result == hashed_password:
31
27
  # Refresh the cache TTL.
32
- refresh_cached_account(username, request.registry)
28
+ cache.expire(cache_key, cache_ttl)
33
29
  return True
34
30
 
35
31
  # Back to standard procedure
@@ -41,53 +37,11 @@ def account_check(username, password, request):
41
37
  except storage_exceptions.ObjectNotFoundError:
42
38
  return None
43
39
 
44
- if validation_enabled and not is_validated(existing):
45
- return None
46
-
47
40
  hashed = existing["password"].encode(encoding="utf-8")
48
41
  pwd_str = password.encode(encoding="utf-8")
49
42
  # Check if password is valid (it is a very expensive computation)
50
43
  if bcrypt.checkpw(pwd_str, hashed):
51
- cache_account(hashed_password, username, request.registry)
52
- return True
53
-
54
- # Last chance, is this a "reset password" flow?
55
- return reset_password_flow(username, password, request)
56
-
57
-
58
- def reset_password_flow(username, password, request):
59
- cache_key = get_account_cache_key(username, request.registry)
60
- hashed_password = utils.hmac_digest(cache_key, password)
61
- pwd_str = password.encode(encoding="utf-8")
62
-
63
- cached_password = get_cached_reset_password(username, request.registry)
64
- if not cached_password:
65
- return None
66
-
67
- # The temporary reset password is only available for changing a user's password.
68
- if request.method.lower() not in ["post", "put", "patch"]:
69
- return None
70
-
71
- # Only allow modifying a user account, no other resource.
72
- uri = utils.strip_uri_prefix(request.path)
73
- resource_name, _ = utils.view_lookup(request, uri)
74
- if resource_name != "account":
75
- return None
76
-
77
- try:
78
- data = request.json["data"]
79
- except (ValueError, KeyError):
80
- return None
81
-
82
- # Request one and only one data field: the `password`.
83
- if not data or "password" not in data or len(data.keys()) > 1:
84
- return None
85
-
86
- cached_password_str = cached_password.encode(encoding="utf-8")
87
- if bcrypt.checkpw(pwd_str, cached_password_str):
88
- # Remove the temporary reset password from the cache.
89
- delete_cached_reset_password(username, request.registry)
90
- cache_account(hashed_password, username, request.registry)
44
+ cache.set(cache_key, hashed_password, ttl=cache_ttl)
91
45
  return True
92
46
 
93
47
 
@@ -1,14 +1,8 @@
1
1
  import bcrypt
2
2
 
3
- from kinto.core import utils
4
-
5
3
 
6
4
  ACCOUNT_CACHE_KEY = "accounts:{}:verified"
7
5
  ACCOUNT_POLICY_NAME = "account"
8
- ACCOUNT_RESET_PASSWORD_CACHE_KEY = "accounts:{}:reset-password"
9
- ACCOUNT_VALIDATION_CACHE_KEY = "accounts:{}:validation-key"
10
- DEFAULT_RESET_PASSWORD_CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
11
- DEFAULT_VALIDATION_KEY_CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
12
6
 
13
7
 
14
8
  def hash_password(password):
@@ -17,130 +11,3 @@ def hash_password(password):
17
11
  pwd_str = password.encode(encoding="utf-8")
18
12
  hashed = bcrypt.hashpw(pwd_str, bcrypt.gensalt())
19
13
  return hashed.decode(encoding="utf-8")
20
-
21
-
22
- def is_validated(user):
23
- """Is this user record validated?"""
24
- # An account is "validated" if it has the `validated` field set to True, or
25
- # no `validated` field at all (for accounts created before the "account
26
- # validation option" was enabled).
27
- return user.get("validated", True)
28
-
29
-
30
- def get_account_cache_key(username, registry):
31
- """Given a username, return the cache key for this account."""
32
- settings = registry.settings
33
- hmac_secret = settings["userid_hmac_secret"]
34
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username))
35
- return cache_key
36
-
37
-
38
- def cache_reset_password(reset_password, username, registry):
39
- """Store a reset-password in the cache."""
40
- settings = registry.settings
41
- hmac_secret = settings["userid_hmac_secret"]
42
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_RESET_PASSWORD_CACHE_KEY.format(username))
43
- # Store a reset password for 7 days by default.
44
- cache_ttl = int(
45
- settings.get(
46
- "account_validation.reset_password_cache_ttl_seconds",
47
- DEFAULT_RESET_PASSWORD_CACHE_TTL_SECONDS,
48
- )
49
- )
50
-
51
- cache = registry.cache
52
- cache_result = cache.set(cache_key, reset_password, ttl=cache_ttl)
53
- return cache_result
54
-
55
-
56
- def get_cached_reset_password(username, registry):
57
- """Given a username, get the reset-password from the cache."""
58
- hmac_secret = registry.settings["userid_hmac_secret"]
59
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_RESET_PASSWORD_CACHE_KEY.format(username))
60
-
61
- cache = registry.cache
62
- cache_result = cache.get(cache_key)
63
- return cache_result
64
-
65
-
66
- def delete_cached_reset_password(username, registry):
67
- """Given a username, delete the reset-password from the cache."""
68
- hmac_secret = registry.settings["userid_hmac_secret"]
69
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_RESET_PASSWORD_CACHE_KEY.format(username))
70
-
71
- cache = registry.cache
72
- cache_result = cache.delete(cache_key)
73
- return cache_result
74
-
75
-
76
- def cache_validation_key(activation_key, username, registry):
77
- """Store a validation_key in the cache."""
78
- settings = registry.settings
79
- hmac_secret = settings["userid_hmac_secret"]
80
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username))
81
- # Store an activation key for 7 days by default.
82
- cache_ttl = int(
83
- settings.get(
84
- "account_validation.validation_key_cache_ttl_seconds",
85
- DEFAULT_VALIDATION_KEY_CACHE_TTL_SECONDS,
86
- )
87
- )
88
-
89
- cache = registry.cache
90
- cache_result = cache.set(cache_key, activation_key, ttl=cache_ttl)
91
- return cache_result
92
-
93
-
94
- def get_cached_validation_key(username, registry):
95
- """Given a username, get the validation key from the cache."""
96
- hmac_secret = registry.settings["userid_hmac_secret"]
97
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username))
98
- cache = registry.cache
99
- activation_key = cache.get(cache_key)
100
- return activation_key
101
-
102
-
103
- def delete_cached_validation_key(username, registry):
104
- """Given a username, delete the validation key from the cache."""
105
- hmac_secret = registry.settings["userid_hmac_secret"]
106
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username))
107
- cache = registry.cache
108
- cache_result = cache.delete(cache_key)
109
- return cache_result
110
-
111
-
112
- def cache_account(hashed_password, username, registry):
113
- """Store an authenticated account in the cache."""
114
- settings = registry.settings
115
- cache_ttl = int(settings.get("account_cache_ttl_seconds", 30))
116
- cache_key = get_account_cache_key(username, registry)
117
- cache = registry.cache
118
- cache_result = cache.set(cache_key, hashed_password, ttl=cache_ttl)
119
- return cache_result
120
-
121
-
122
- def get_cached_account(username, registry):
123
- """Given a username, get the account from the cache."""
124
- cache_key = get_account_cache_key(username, registry)
125
- cache = registry.cache
126
- cached_account = cache.get(cache_key)
127
- return cached_account
128
-
129
-
130
- def refresh_cached_account(username, registry):
131
- """Given a username, refresh the cache TTL."""
132
- settings = registry.settings
133
- cache_ttl = int(settings.get("account_cache_ttl_seconds", 30))
134
- cache_key = get_account_cache_key(username, registry)
135
- cache = registry.cache
136
- cache_result = cache.expire(cache_key, cache_ttl)
137
- return cache_result
138
-
139
-
140
- def delete_cached_account(username, registry):
141
- """Given a username, delete the account key from the cache."""
142
- hmac_secret = registry.settings["userid_hmac_secret"]
143
- cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username))
144
- cache = registry.cache
145
- cache_result = cache.delete(cache_key)
146
- return cache_result