33
44import json
55import 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
810from odoo .tools ._vendor .sessions import SessionStore
911
1012from . 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
1516DEFAULT_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+
2029class 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