Skip to content

Commit 2eb1d13

Browse files
committed
Merge commit 'refs/pull/499/head' of github.com:camptocamp/odoo-cloud-platform into merge-branch-3525-add-pending-module-19-380157c8
2 parents c44393e + 48a502b commit 2eb1d13

File tree

3 files changed

+109
-17
lines changed

3 files changed

+109
-17
lines changed

session_redis/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
{
66
"name": "Sessions in Redis",
77
"summary": "Store web sessions in Redis",
8-
"version": "18.0.1.0.0",
8+
"version": "19.0.1.0.0",
99
"author": "Camptocamp,Odoo Community Association (OCA)",
1010
"license": "AGPL-3",
1111
"category": "Extra Tools",

session_redis/http.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Copyright 2016-2024 Camptocamp SA
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3-
3+
import functools
44
import logging
55
import os
66

77
from odoo import http
88
from odoo.tools import config
9-
from odoo.tools.func import lazy_property
109

1110
from .session import RedisSessionStore
1211
from .strtobool import strtobool
@@ -46,7 +45,7 @@ def is_true(strval):
4645
redis_cluster = os.getenv("ODOO_SESSION_REDIS_CLUSTER", "0")
4746

4847

49-
@lazy_property
48+
@functools.cached_property
5049
def session_store(self):
5150
if sentinel_host:
5251
sentinel = Sentinel([(sentinel_host, sentinel_port)], password=password)
@@ -108,5 +107,14 @@ def purge_fs_sessions(path):
108107
port,
109108
)
110109
http.Application.session_store = session_store
110+
# cached_property needs __set_name__ to be called, but it is not called
111+
# automatically since we are attaching the property after instance creation.
112+
# So we have to do it manually
113+
# See: https://docs.python.org/3/reference/datamodel.html#object.__set_name__
114+
# Credit: https://stackoverflow.com/a/62161136
115+
http.Application.session_store.__set_name__(
116+
http.Application,
117+
"session_store",
118+
)
111119
# clean the existing sessions on the file system
112120
purge_fs_sessions(config.session_dir)

session_redis/session.py

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@
33

44
import json
55
import logging
6+
from typing import TypeAlias, List
67

7-
from odoo.service import security
8+
import odoo.http
9+
from odoo.http import SESSION_LIFETIME
810
from odoo.tools._vendor.sessions import SessionStore
911

1012
from . import json_encoding
1113

12-
# this is equal to the duration of the session garbage collector in
14+
# this was equal to the duration of the session garbage collector in
1315
# odoo.http.session_gc()
14-
DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 * 7 # 7 days in seconds
1516
DEFAULT_SESSION_TIMEOUT_ANONYMOUS = 60 * 60 * 3 # 3 hours in seconds
1617

1718
_logger = logging.getLogger(__name__)
1819

1920

21+
# Many parts of the session store API operate not on full session keys, but only
22+
# the first n characters of them (see odoo.http.STORED_SESSION_BYTES). In
23+
# particular used by Devices, but Odoo in general seems to promise that this
24+
# partial sid will be safe to store in the database, and can be used to later
25+
# find sessions, even if those sessions are actually longer.
26+
PartialSid: TypeAlias = str
27+
28+
2029
class RedisSessionStore(SessionStore):
2130
"""SessionStore that saves session to redis"""
2231

@@ -31,7 +40,7 @@ def __init__(
3140
super().__init__(session_class=session_class)
3241
self.redis = redis
3342
if expiration is None:
34-
self.expiration = DEFAULT_SESSION_TIMEOUT
43+
self.expiration = SESSION_LIFETIME
3544
else:
3645
self.expiration = expiration
3746
if anon_expiration is None:
@@ -42,18 +51,37 @@ def __init__(
4251
if prefix:
4352
self.prefix = f"{self.prefix}:{prefix}:"
4453

54+
# Use the key generation method of the FileSystemSessionStore: it seems that
55+
# the one on the general SessionStore does not generate long enough keys to
56+
# support the device session rotation logic (SessionStore produces 40
57+
# character long keys, while the new rotation logic appears to assume a
58+
# length of at least 84).
59+
generate_key = odoo.http.FilesystemSessionStore.generate_key
60+
is_valid_key = odoo.http.FilesystemSessionStore.is_valid_key
61+
4562
def build_key(self, sid):
4663
return f"{self.prefix}{sid}"
4764

4865
def save(self, session):
4966
key = self.build_key(session.sid)
5067

51-
# allow to set a custom expiration for a session
68+
# If the session has a deletion_time, it is slated for rotation, and
69+
# should be removed once the rotation window is over. See
70+
# odoo.http.SESSION_DELETION_TIMER.
71+
# Otherwise, allow to set a custom expiration for a session
5272
# such as a very short one for monitoring requests
5373
if session.uid:
54-
expiration = session.expiration or self.expiration
74+
expiration = (
75+
session.get("deletion_time")
76+
or session.get("expiration")
77+
or self.expiration
78+
)
5579
else:
56-
expiration = session.expiration or self.anon_expiration
80+
expiration = (
81+
session.get("deletion_time")
82+
or session.get("expiration")
83+
or self.anon_expiration
84+
)
5785
if _logger.isEnabledFor(logging.DEBUG):
5886
if session.uid:
5987
user_msg = f"user '{session.login}' (id: {session.uid})"
@@ -106,12 +134,9 @@ def list(self):
106134
_logger.debug("a listing redis keys has been called")
107135
return [key[len(self.prefix) :] for key in keys]
108136

109-
def rotate(self, session, env):
110-
self.delete(session)
111-
session.sid = self.generate_key()
112-
if session.uid and env:
113-
session.session_token = security.compute_session_token(session, env)
114-
self.save(session)
137+
# The FilesystemSessionStore's rotate does not do anything file-system
138+
# specific so it can just be reused here
139+
rotate = odoo.http.FilesystemSessionStore.rotate
115140

116141
def vacuum(self, *args, **kwargs):
117142
"""Do not garbage collect the sessions
@@ -120,3 +145,62 @@ def vacuum(self, *args, **kwargs):
120145
expiration.
121146
"""
122147
return None
148+
149+
def delete_old_sessions(self, session):
150+
"""
151+
# Deletion of rotated sessions is handled by updating the sessions'
152+
# expiry based on deletion_time in save(), so this method is redundant
153+
# when using a redis store.
154+
155+
# While this method is not part of the generic SessionStore API, it is
156+
# defined on the file session store, and is used by the Session itself
157+
# as part of the session rotation (see odoo.http.Session._delete_old_sessions).
158+
"""
159+
return
160+
161+
def get_missing_session_identifiers(self, identifiers: List[PartialSid]) -> set[PartialSid]:
162+
"""
163+
Given a list of partial session ids, return a set of those session ids
164+
which no longer exist in the keystore.
165+
166+
While this method is not part of the generic SessionStore API, it is
167+
defined on the file session store, and is used by Odoo's devices to
168+
figure out what needs to be revoked
169+
(see odoo.addons.base.models.res_device.ResDeviceLog.__update_revoked).
170+
"""
171+
identifiers = set(identifiers)
172+
not_found = set()
173+
for partial_sid in identifiers:
174+
try:
175+
next(
176+
self.redis.scan_iter(
177+
match=f"{self.prefix}{partial_sid}*",
178+
count=1,
179+
)
180+
)
181+
except StopIteration:
182+
# No matches found
183+
not_found.add(partial_sid)
184+
185+
return not_found
186+
187+
def delete_from_identifiers(self, identifiers: List[PartialSid]):
188+
"""
189+
Given a list of partial session ids, remove any that are in the session store.
190+
191+
While this method is not part of the generic SessionStore API, it is
192+
defined on the file session store, and is used by devices when revoking
193+
device sessions (see odoo.addons.base.models.res_device.ResDevice._revoke).
194+
"""
195+
patterns_to_unlink = []
196+
for identifier in identifiers:
197+
# Avoid removing a session if it does not match an identifier.
198+
# See this same comment in odoo.http.FileSessionStore.delete_from_identifiers.
199+
if not odoo.http._session_identifier_re.match(identifier):
200+
raise ValueError("Identifier format incorrect, did you pass in a string instead of a list?")
201+
patterns_to_unlink.append(f"{self.prefix}{identifier}*")
202+
keys_to_unlink = []
203+
for pattern in patterns_to_unlink:
204+
keys_to_unlink.extend(self.redis.scan_iter(match=pattern))
205+
if keys_to_unlink:
206+
self.redis.delete(*keys_to_unlink)

0 commit comments

Comments
 (0)