mangono-addon-redis_session_store 2.2.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.
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: mangono-addon-redis_session_store
3
+ Version: 2.2.0
4
+ Project-URL: Homepage, http://mangono.fr/
5
+ Author-Email: mangono <opensource+odoo@mangono.fr>
6
+ License-Expression: AGPL-3.0
7
+ Classifier: Framework :: Odoo
8
+ Classifier: Framework :: Odoo :: 12.0
9
+ Classifier: Framework :: Odoo :: 13.0
10
+ Classifier: Framework :: Odoo :: 14.0
11
+ Classifier: Framework :: Odoo :: 15.0
12
+ Classifier: Framework :: Odoo :: 16.0
13
+ Classifier: Framework :: Odoo :: 17.0
14
+ Classifier: Framework :: Odoo :: 18.0
15
+ Classifier: Framework :: Odoo :: 19.0
16
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.8
25
+ Classifier: Programming Language :: Python :: 3.9
26
+ Requires-Dist: redis==5.2.1
27
+ Requires-Dist: environ-odoo-config<2,>=0.6.5
28
+ Requires-Dist: typing-extensions
29
+ Requires-Dist: wrapt==1.17.3
30
+ Description-Content-Type: text/plain
31
+ Description: Use Redis Session instead of File system to store sessions
32
+
@@ -0,0 +1,14 @@
1
+ odoo/addons/redis_session_store/__init__.py,sha256=OY0COk3W8fkudSBfPqdrOVDba4pPQaMsxF6J3S0NjdY,1146
2
+ odoo/addons/redis_session_store/__manifest__.py,sha256=0FZYxlcFNZh2zTsnoZ7kHKh9Rm3QHGwEIHHl8rKLIdg,488
3
+ odoo/addons/redis_session_store/env_config.py,sha256=dHWpucfThhyRaUSzdP1NCNWSk0GMyrHvnDA-yYrUEqs,2730
4
+ odoo/addons/redis_session_store/json_encoding.py,sha256=Zb9MqFWi9aCCaWml5ErXlNtmDqmfUK1C6fD0MRRa_ok,1219
5
+ odoo/addons/redis_session_store/odoo_monkey_patch.py,sha256=5MpZASEbF6ZjMUPP_trxbZg5HmS7EL_VVA793THykfY,2243
6
+ odoo/addons/redis_session_store/redis_session.py,sha256=sLlOeJ3NWJQ0SP8uF-7l1uPyXKZRApR4lJny9b68Q7c,9670
7
+ odoo/addons/redis_session_store/environ_odoo_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ odoo/addons/redis_session_store/environ_odoo_config/auto_load.py,sha256=FpE2fsxPn04E20xVSe7cumf1weUl5-x4NpsIFbZIfHE,256
9
+ odoo/addons/redis_session_store/environ_odoo_config/mapper.py,sha256=rsh5D6tF4ucsdBgBAQmm5e4mRMWKz4AoaaAXltT60yE,452
10
+ mangono_addon_redis_session_store-2.2.0.dist-info/METADATA,sha256=fbFTKnvHL5qjC1mi6PHovCBAeE8Tlue9TajSASeM_us,1305
11
+ mangono_addon_redis_session_store-2.2.0.dist-info/WHEEL,sha256=UYBUuRA3yPiZ7Eiy54KdZLBuO4pAoV-5dppQ0JKx5y4,98
12
+ mangono_addon_redis_session_store-2.2.0.dist-info/entry_points.txt,sha256=VCn3ggBV8ioYxOYvqhOc1cDvuOEnskUk5_JSE1FHevE,278
13
+ mangono_addon_redis_session_store-2.2.0.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
14
+ mangono_addon_redis_session_store-2.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: Mangono Wheel Builder0.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,6 @@
1
+ [environ_odoo_config.mapper]
2
+ redis_mapper = odoo.addons.redis_session_store.environ_odoo_config.mapper:redis_mapper
3
+
4
+ [environ_odoo_config.auto_server_wide_module]
5
+ redis_session_store = odoo.addons.redis_session_store.environ_odoo_config.auto_load:auto_load_redis_session_store
6
+
@@ -0,0 +1,34 @@
1
+ import logging
2
+ from environ_odoo_config import odoo_utils
3
+
4
+ _logger = logging.getLogger("odoo.session.REDIS")
5
+
6
+ try:
7
+ import redis
8
+
9
+ _logger.info("Lib redis installed")
10
+ except ImportError:
11
+ redis = None
12
+
13
+
14
+ def _post_load_module():
15
+ if not redis:
16
+ raise ImportError("Please install package redis")
17
+ import odoo.release
18
+
19
+ if "redis_session_store" not in odoo_utils.get_server_wide_modules(odoo.release.serie):
20
+ return
21
+ from environ_odoo_config.environ import Environ
22
+ from .env_config import RedisEnvConfig
23
+
24
+ redis_config = RedisEnvConfig(Environ.new())
25
+ server_info = redis_config.connect().info()
26
+ # In case this is a Materia KV Redis compatible databaseOdooSessionClass
27
+ if not server_info.get("redis_version") and server_info.get("Materia KV "):
28
+ server_info = {"redis_version": f"Materia KV - {server_info['Materia KV ']}"}
29
+ if not server_info:
30
+ raise ValueError("Can't display server info")
31
+ _logger.info("Redis Session enable [%s]", server_info.get("redis_version", "No version provided"))
32
+ from . import odoo_monkey_patch
33
+
34
+ odoo_monkey_patch.patch_odoo(redis_config)
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "Redis Session Store",
3
+ "version": "2.2.0",
4
+ "depends": ["base"],
5
+ "author": "mangono",
6
+ "website": "http://mangono.fr/",
7
+ "license": "AGPL-3",
8
+ "description": """Use Redis Session instead of File system to store sessions""",
9
+ "summary": "",
10
+ "category": "Tools",
11
+ "auto_install": False,
12
+ "installable": False,
13
+ "application": False,
14
+ "post_load": "_post_load_module",
15
+ "external_dependencies": {
16
+ "python": ["redis"],
17
+ },
18
+ }
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import redis
4
+ from environ_odoo_config.config_section.api import OdooConfigGroup, RepeatableKey, SimpleKey
5
+ from environ_odoo_config.environ import Environ
6
+ from environ_odoo_config.odoo_version import OdooVersion
7
+
8
+ DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 * 3 # 3 days in seconds
9
+ DEFAULT_SESSION_TIMEOUT_ANONYMOUS = 60 * 2 # 2 minutes in seconds
10
+ DEFAULT_SESSION_TIMEOUT_ON_INACTIVITY = "True"
11
+ DEFAULT_SESSION_TIMEOUT_IGNORED_URLS = ["/longpolling", "/calendar/notify"]
12
+
13
+
14
+ class RedisEnvConfig(OdooConfigGroup):
15
+ _ini_section = "redis_session"
16
+
17
+ host: str = SimpleKey("REDIS_HOST", ini_dest="redis_host", py_default="localhost")
18
+ port: int = SimpleKey("REDIS_PORT", ini_dest="redis_port", py_default=6379)
19
+ prefix: str = SimpleKey("REDIS_PREFIX", ini_dest="redis_prefix")
20
+ url: str = SimpleKey("REDIS_URL", ini_dest="redis_url")
21
+ password: str = SimpleKey("REDIS_PASSWORD", ini_dest="redis_password")
22
+ redis_ssl: bool = SimpleKey(
23
+ "REDIS_SSL",
24
+ ini_dest="redis_ssl",
25
+ py_default=True,
26
+ )
27
+ expiration: int = SimpleKey(
28
+ "ODOO_SESSION_REDIS_EXPIRATION", ini_dest="redis_expiration", py_default=DEFAULT_SESSION_TIMEOUT
29
+ )
30
+ anon_expiration: int = SimpleKey(
31
+ "ODOO_SESSION_REDIS_EXPIRATION_ANONYMOUS",
32
+ ini_dest="redis_anon_expiration",
33
+ py_default=DEFAULT_SESSION_TIMEOUT_ANONYMOUS,
34
+ )
35
+ timeout_on_inactivity: bool = SimpleKey(
36
+ "ODOO_SESSION_REDIS_TIMEOUT_ON_INACTIVITY",
37
+ ini_dest="redis_timeout_on_inactivity",
38
+ py_default=DEFAULT_SESSION_TIMEOUT_ON_INACTIVITY,
39
+ )
40
+ ignored_urls: set[str] = RepeatableKey(
41
+ "ODOO_SESSION_REDIS_TIMEOUT_IGNORED_URLS",
42
+ ini_dest="redis_ignored_urls",
43
+ ini_default=DEFAULT_SESSION_TIMEOUT_IGNORED_URLS,
44
+ )
45
+ db: int = SimpleKey("REDIS_DB_INDEX", py_default=0)
46
+ disable_gc: bool = SimpleKey("ODOO_DISABLE_SESSION_GC")
47
+
48
+ @property
49
+ def enable(self) -> bool:
50
+ return bool(self.host and self.password)
51
+
52
+ def _post_parse_env(self, environ: Environ):
53
+ if self.for_version > OdooVersion.V16:
54
+ self.disable_gc = True
55
+
56
+ def connect(self) -> redis.Redis:
57
+ """
58
+ Return the connection to the Redis server.
59
+ If `self.url` is filled, then `url` all other connection info are exclude.
60
+
61
+ expiration, and anon_expiration are not passed to the connection.
62
+
63
+ :return: A connection to the Redis server.
64
+ """
65
+ if self.url:
66
+ return redis.Redis.from_url(self.url)
67
+ return redis.Redis(
68
+ host=self.host,
69
+ port=self.port,
70
+ db=self.db,
71
+ password=self.password,
72
+ ssl=self.redis_ssl,
73
+ )
@@ -0,0 +1,8 @@
1
+ from environ_odoo_config.environ import Environ
2
+
3
+ from odoo.addons.redis_session_store.env_config import RedisEnvConfig
4
+
5
+
6
+ def auto_load_redis_session_store(environ: Environ) -> bool:
7
+ redis_config = RedisEnvConfig(environ)
8
+ return redis_config.enable
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from environ_odoo_config.environ import Environ
6
+
7
+
8
+ def redis_mapper(curr_env: Environ | dict[str, Any]) -> Environ:
9
+ return _kv_clevercloud_redis(Environ(curr_env))
10
+
11
+
12
+ def _kv_clevercloud_redis(curr_env: Environ) -> Environ:
13
+ """ """
14
+ return curr_env + {
15
+ "REDIS_HOST": curr_env.gets("REDIS_HOST", "KV_HOST"),
16
+ "REDIS_PORT": curr_env.gets("REDIS_PORT", "KV_PORT"),
17
+ }
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import date, datetime
5
+
6
+ import dateutil
7
+
8
+
9
+ def _object_decoder(obj):
10
+ if "_type" not in obj:
11
+ return obj
12
+ type_ = obj["_type"]
13
+ if type_ == "datetime_isoformat":
14
+ return dateutil.parser.parse(obj["value"])
15
+ elif type_ == "date_isoformat":
16
+ return dateutil.parser.parse(obj["value"]).date()
17
+ elif type_ == "set":
18
+ return set(obj["value"])
19
+ return obj
20
+
21
+
22
+ class SessionEncoder(json.JSONEncoder):
23
+ """Encode date/datetime objects
24
+
25
+ So that we can later recompose them if they were stored in the session
26
+ """
27
+
28
+ def default(self, obj):
29
+ if isinstance(obj, datetime):
30
+ return {"_type": "datetime_isoformat", "value": obj.isoformat()}
31
+ elif isinstance(obj, date):
32
+ return {"_type": "date_isoformat", "value": obj.isoformat()}
33
+ elif isinstance(obj, set):
34
+ return {"_type": "set", "value": tuple(obj)}
35
+ return json.JSONEncoder.default(self, obj)
36
+
37
+
38
+ class SessionDecoder(json.JSONDecoder):
39
+ """Decode json, recomposing recordsets and date/datetime"""
40
+
41
+ def __init__(self, **kwargs):
42
+ super().__init__(object_hook=_object_decoder, **kwargs)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import random
5
+ from typing import Any
6
+
7
+ import wrapt
8
+
9
+ import odoo
10
+ from odoo import http as odoo_http
11
+ from odoo.http import request
12
+ from odoo.tools import func as odoo_func
13
+
14
+ from .env_config import RedisEnvConfig
15
+ from .redis_session import RedisSessionStore
16
+
17
+ _logger = logging.getLogger("odoo.session.REDIS")
18
+
19
+
20
+ def _get_session_class(major_odoo_version: int):
21
+ if major_odoo_version < 16:
22
+ return odoo_http.OpenERPSession
23
+ return odoo_http.Session
24
+
25
+
26
+ def _reset_cached_properties(major_odoo_version: int, obj: Any):
27
+ if major_odoo_version >= 19:
28
+ odoo_func.reset_cached_properties(obj)
29
+ else:
30
+ odoo_func.lazy_property.reset_all(obj)
31
+
32
+
33
+ def session_gc(session_store):
34
+ # session_gc is called at setup_session so we keep the randomness bit to only vacuum once in a while.
35
+ if random.random() < 0.001:
36
+ session_store.vacuum()
37
+
38
+
39
+ def _update_expiration():
40
+ if (
41
+ hasattr(odoo_http.root.session_store, "update_expiration")
42
+ and request
43
+ and request.session
44
+ and request.session.uid
45
+ and not request.env["res.users"].browse(request.session.uid)._is_public()
46
+ ):
47
+ odoo_http.root.session_store.update_expiration(request.session)
48
+
49
+
50
+ def patch_odoo(redis_config: RedisEnvConfig):
51
+ _patch_odoo_http_root(redis_config)
52
+ _patch_odoo_authenticate(redis_config)
53
+
54
+
55
+ def _patch_odoo_http_root(redis_config: RedisEnvConfig):
56
+ _reset_cached_properties(redis_config.for_version.value, odoo.http.root)
57
+ type(odoo_http.root).session_store = RedisSessionStore(
58
+ redis_config, session_class=_get_session_class(redis_config.for_version.value)
59
+ )
60
+ # Keep compatibility with odoo env config.
61
+ # There is no more session_gc global function, so no more patch needed.
62
+ # Now see FilesystemSessionStore#vacuum.
63
+ if not redis_config.disable_gc:
64
+ odoo_http.session_gc = session_gc
65
+
66
+
67
+ def _patch_odoo_authenticate(redis_config: RedisEnvConfig):
68
+ @wrapt.patch_function_wrapper(
69
+ "odoo.addons.base",
70
+ "models.ir_http.IrHttp._authenticate",
71
+ )
72
+ def _patch_IrHttp__authenticate(wrapped, instance, args, kwargs):
73
+ _update_expiration()
74
+ return wrapped(*args, **kwargs)
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import logging
6
+ import os
7
+ import re
8
+ import time
9
+ import warnings
10
+ from hashlib import sha512
11
+
12
+ import odoo
13
+ from odoo import http
14
+ from odoo.service import security
15
+
16
+ from . import json_encoding
17
+ from .env_config import RedisEnvConfig
18
+
19
+ # The amount of bytes of the session that will remain static and can be used
20
+ # for calculating the csrf token and be stored inside the database.
21
+ STORED_SESSION_BYTES = 42
22
+ # After a session is rotated, the session should be kept for a couple of
23
+ # seconds to account for network delay between multiple requests which are
24
+ # made at the same time and all use the same old cookie.
25
+ SESSION_DELETION_TIMER = 120
26
+ MAJOR = odoo.release.version_info[0]
27
+ try:
28
+ with warnings.catch_warnings(record=True):
29
+ import werkzeug.contrib.sessions as sessions
30
+ except ImportError:
31
+ from odoo.tools._vendor import sessions
32
+
33
+
34
+ _logger = logging.getLogger("odoo.session.REDIS")
35
+
36
+
37
+ # this is equal to the duration of the session garbage collector in
38
+ # odoo.http.session_gc()
39
+
40
+
41
+ _logger = logging.getLogger(__name__)
42
+ _base64_urlsafe_re = re.compile(r"^[A-Za-z0-9_-]{84}$")
43
+ _session_identifier_re = re.compile(r"^[A-Za-z0-9_-]{%s}$" % STORED_SESSION_BYTES) # noqa
44
+
45
+
46
+ class RedisSessionStore(sessions.SessionStore):
47
+ """SessionStore that saves session to redis"""
48
+
49
+ def __init__(
50
+ self,
51
+ redis_config: RedisEnvConfig,
52
+ session_class,
53
+ ):
54
+ super().__init__(session_class=session_class)
55
+ self.redis = redis_config.connect()
56
+ self.expiration = redis_config.expiration
57
+ self.anon_expiration = redis_config.anon_expiration
58
+ self.timeout_on_inactivity = redis_config.timeout_on_inactivity
59
+ self.ignored_urls = redis_config.ignored_urls
60
+ self.support_expire = b"expire" in self.redis.command_list()
61
+ self.prefix = "session:"
62
+ if redis_config.prefix:
63
+ self.prefix = f"{self.prefix}:{redis_config.prefix}:"
64
+
65
+ def build_key(self, sid):
66
+ return f"{self.prefix}{sid}"
67
+
68
+ def is_valid_key(self, key):
69
+ return _base64_urlsafe_re.match(key) is not None
70
+
71
+ def get_expiration(self, session):
72
+ # session.expiration allow to set a custom expiration for a session
73
+ # such as a very short one for monitoring requests
74
+ if not self.support_expire:
75
+ return -1
76
+ session_expiration = getattr(session, "expiration", 0)
77
+ expiration = session_expiration or self.anon_expiration
78
+ if session.uid:
79
+ expiration = session_expiration or self.expiration
80
+ return expiration
81
+
82
+ def update_expiration(self, session):
83
+ if not self.support_expire or not self.timeout_on_inactivity:
84
+ return
85
+ path = http.request.httprequest.path
86
+ if any(path.startswith(url) for url in self.ignored_urls):
87
+ return
88
+ key = self.build_key(session.sid)
89
+ expiration = self.get_expiration(session)
90
+
91
+ return self.redis.expire(key, expiration)
92
+
93
+ def save(self, session):
94
+ key = self.build_key(session.sid)
95
+ expiration = self.get_expiration(session)
96
+ if _logger.isEnabledFor(logging.DEBUG):
97
+ if session.uid:
98
+ user_msg = f"user '{session.login}' (id: {session.uid})"
99
+ else:
100
+ user_msg = "anonymous user"
101
+ _logger.debug(
102
+ "saving session with key '%s' and expiration of %s seconds for %s",
103
+ key,
104
+ expiration,
105
+ user_msg,
106
+ )
107
+
108
+ data = json.dumps(dict(session), cls=json_encoding.SessionEncoder).encode("utf-8")
109
+ result = self.redis.set(key, data)
110
+ if result and self.support_expire:
111
+ return self.redis.expire(key, expiration)
112
+ return -1
113
+
114
+ def delete(self, session):
115
+ key = self.build_key(session.sid)
116
+ _logger.debug("deleting session with key %s", key)
117
+ return self.redis.delete(key)
118
+
119
+ def delete_old_sessions(self, session):
120
+ pass
121
+
122
+ def get(self, sid):
123
+ if not self.is_valid_key(sid):
124
+ _logger.debug(
125
+ "session with invalid sid '%s' has been asked, returning a new one",
126
+ sid,
127
+ )
128
+ return self.new()
129
+
130
+ key = self.build_key(sid)
131
+ saved = self.redis.get(key)
132
+ if not saved:
133
+ _logger.debug(
134
+ "session with non-existent key '%s' has been asked, returning a new one",
135
+ key,
136
+ )
137
+ return self.new()
138
+ try:
139
+ data = json.loads(saved.decode("utf-8"), cls=json_encoding.SessionDecoder)
140
+ except ValueError:
141
+ _logger.debug(
142
+ "session for key '%s' has been asked but its json content could not be read, it has been reset",
143
+ key,
144
+ )
145
+ data = {}
146
+ return self.session_class(data, sid, False)
147
+
148
+ def list(self):
149
+ keys = self.redis.keys(f"{self.prefix}*")
150
+ _logger.debug("a listing redis keys has been called")
151
+ return [key[len(self.prefix) :] for key in keys]
152
+
153
+ def rotate(self, session, env, soft=False):
154
+ if soft:
155
+ # Multiple network requests can occur at the same time, all using the old session.
156
+ # We don't want to create a new session for each request, it's better to reference the one already made.
157
+ static = session.sid[:STORED_SESSION_BYTES]
158
+ recent_session = self.get(session.sid)
159
+ if "next_sid" in recent_session:
160
+ # A new session has already been saved on disk by a concurrent request,
161
+ # the _save_session is going to simply use session.sid to set a new cookie.
162
+ session.sid = recent_session["next_sid"]
163
+ return
164
+ next_sid = static + self.generate_key()[STORED_SESSION_BYTES:]
165
+ session["next_sid"] = next_sid
166
+ session["deletion_time"] = time.time() + SESSION_DELETION_TIMER
167
+ self.save(session)
168
+ # Now prepare the new session
169
+ session["gc_previous_sessions"] = True
170
+ session.sid = next_sid
171
+ del session["deletion_time"]
172
+ del session["next_sid"]
173
+ else:
174
+ self.delete(session)
175
+ session.sid = self.generate_key()
176
+ if session.uid:
177
+ assert env, "saving this session requires an environment"
178
+ session.session_token = security.compute_session_token(session, env)
179
+ session.should_rotate = False
180
+ session["create_time"] = time.time()
181
+ self.save(session)
182
+
183
+ def vacuum(self, *args, **kwargs):
184
+ """
185
+ Vacuum all expired keys.
186
+ """
187
+ # For MateriaKV, there is currently no active expiration. But `DBSIZE` seems to trigger the database gc.
188
+ # https://www.clever.cloud/developers/doc/addons/materia-kv/
189
+ # Useless for pure Redis config since there is an active expiration process. See :
190
+ # https://redis.io/docs/latest/commands/expire/#how-redis-expires-keys
191
+ self.redis.dbsize()
192
+ _logger.debug("retrieving dbsize to trigger keys vacuum")
193
+ return True
194
+
195
+ def get_missing_session_identifiers(self, identifiers: list[str]) -> list[str]:
196
+ """
197
+ :param identifiers: session identifiers whose file existence must be checked
198
+ identifiers are a part session sid (first 42 chars)
199
+ :type identifiers: iterable
200
+ :return: the identifiers which are not present on the filesystem
201
+ :rtype: set
202
+ """
203
+ existing_keys = set()
204
+ for key in identifiers:
205
+ if self.redis.exists(self.build_key(key)):
206
+ existing_keys.add(key)
207
+ # Remove the identifiers for which a key is present on the session store.
208
+ missing_keys = set(identifiers) - existing_keys
209
+ return list(missing_keys)
210
+
211
+ def delete_from_identifiers(self, identifiers: list[str]):
212
+ for identifier in identifiers:
213
+ # Avoid to remove a session if it does not match an identifier.
214
+ # This prevents malicious user to delete sessions from a different
215
+ # database by specifying a custom ``res.device.log``.
216
+ if not _session_identifier_re.match(identifier):
217
+ raise ValueError(
218
+ f"Identifier format incorrect, did you pass in a string instead of a list? {identifier}"
219
+ )
220
+ redis_key = self.build_key(identifier)
221
+ self.redis.delete(redis_key)
222
+
223
+ def generate_key(self, salt=None):
224
+ # The generated key is case sensitive (base64) and the length is 84 chars.
225
+ # In the worst-case scenario, i.e. in an insensitive filesystem (NTFS for example)
226
+ # taking into account the proportion of characters in the pool and a length
227
+ # of 42 (stored part in the database), the entropy for the base64 generated key
228
+ # is 217.875 bits which is better than the 160 bits entropy of a hexadecimal key
229
+ # with a length of 40 (method ``generate_key`` of ``SessionStore``).
230
+ # The risk of collision is negligible in practice.
231
+ # Formulas:
232
+ # - L: length of generated word
233
+ # - p_char: probability of obtaining the character in the pool
234
+ # - n: size of the pool
235
+ # - k: number of generated word
236
+ # Entropy = - L * sum(p_char * log2(p_char))
237
+ # Collision ~= (1 - exp((-k * (k - 1)) / (2 * (n**L))))
238
+ key = str(time.time()).encode() + os.urandom(64)
239
+ hash_key = sha512(key).digest()[:-1] # prevent base64 padding
240
+ return base64.urlsafe_b64encode(hash_key).decode("utf-8")