Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/session.py: 29%
89 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +0000
1import hmac
2import logging
3import time
4from viur.core.tasks import DeleteEntitiesIter
5from viur.core.config import conf # this import has to stay alone due partial import
6from viur.core import db, utils, tasks
7import typing as t
9"""
10 Provides the session implementation for the Google AppEngine™ based on the datastore.
11 To access the current session, and call current.session.get()
13 Example:
15 .. code-block:: python
17 from viur.core import current
18 sessionData = current.session.get()
19 sessionData["your_key"] = "your_data"
20 data = sessionData["your_key"]
22 A get-method is provided for convenience.
23 It returns None instead of raising an Exception if the key is not found.
24"""
27class Session:
28 """
29 Store Sessions inside the datastore.
30 The behaviour of this module can be customized in the following ways:
32 - :prop:same_site can be set to None, "none", "lax" or "strict" to influence the same-site tag on the cookies
33 we set
34 - :prop:use_session_cookie is set to True by default, causing the cookie to be treated as a session cookie
35 (it will be deleted on browser close). If set to False, it will be emitted with the life-time in
36 conf.user.session_life_time.
37 - The config variable conf.user.session_life_time: Determines, how long (in seconds) a session is valid.
38 Even if :prop:use_session_cookie is set to True, the session is voided server-side after no request has been
39 made within the configured lifetime.
40 - The config variables conf.user.session_persistent_fields_on_login and
41 conf.user.session_persistent_fields_on_logout lists fields, that may survive a login/logout action.
42 For security reasons, we completely destroy a session on login/logout (it will be deleted, a new empty
43 database object will be created and a new cookie with a different key is sent to the browser). This causes
44 all data currently stored to be lost. Only keys listed in these variables will be copied into the new
45 session.
46 """
47 kindName = "viur-session"
48 same_site = "lax" # Either None (don't issue same_site header), "none", "lax" or "strict"
49 use_session_cookie = True # If True, issue the cookie without a lifeTime (will disappear on browser close)
50 cookie_name = f"""viur_cookie_{conf.instance.project_id}"""
51 GUEST_USER = "__guest__"
53 def __init__(self):
54 super().__init__()
55 self.changed = False
56 self.cookie_key = None
57 self.static_security_key = None
58 self.session = db.Entity()
60 def load(self, req):
61 """
62 Initializes the Session.
64 If the client supplied a valid Cookie, the session is read from the datastore, otherwise a new,
65 empty session will be initialized.
66 """
67 if cookie_key := str(req.request.cookies.get(self.cookie_name)):
68 if data := db.Get(db.Key(self.kindName, cookie_key)): # Loaded successfully
69 if data["lastseen"] < time.time() - conf.user.session_life_time:
70 # This session is too old
71 self.reset()
72 return False
74 self.cookie_key = cookie_key
75 self.session = data["data"]
76 self.static_security_key = data.get("static_security_key") or data.get("staticSecurityKey")
78 if data["lastseen"] < time.time() - 5 * 60: # Refresh every 5 Minutes
79 self.changed = True
80 else:
81 self.reset()
82 else:
83 self.reset()
85 def save(self, req):
86 """
87 Writes the session into the database.
89 Does nothing, in case the session hasn't been changed in the current request.
90 """
91 if not self.changed:
92 return
94 # We will not issue sessions over http anymore
95 if not (req.isSSLConnection or conf.instance.is_dev_server):
96 return
98 # Get the current user's key
99 try:
100 # Check for our custom user-api
101 user_key = conf.main_app.vi.user.getCurrentUser()["key"]
102 except Exception:
103 user_key = Session.GUEST_USER # this is a guest
105 dbSession = db.Entity(db.Key(self.kindName, self.cookie_key))
107 dbSession["data"] = db.fixUnindexableProperties(self.session)
108 dbSession["static_security_key"] = self.static_security_key
109 dbSession["lastseen"] = time.time()
110 dbSession["user"] = str(user_key) # allow filtering for users
111 dbSession.exclude_from_indexes = {"data"}
113 db.Put(dbSession)
115 # Provide Set-Cookie header entry with configured properties
116 flags = (
117 "Path=/",
118 "HttpOnly",
119 f"SameSite={self.same_site}" if self.same_site and not conf.instance.is_dev_server else None,
120 "Secure" if not conf.instance.is_dev_server else None,
121 f"Max-Age={conf.user.session_life_time}" if not self.use_session_cookie else None,
122 )
124 req.response.headerlist.append(
125 ("Set-Cookie", f"{self.cookie_name}={self.cookie_key};{';'.join([f for f in flags if f])}")
126 )
128 def __contains__(self, key: str) -> bool:
129 """
130 Returns True if the given *key* is set in the current session.
131 """
132 return key in self.session
134 def __delitem__(self, key: str) -> None:
135 """
136 Removes a *key* from the session.
138 This key must exist.
139 """
140 del self.session[key]
141 self.changed = True
143 def __getitem__(self, key) -> t.Any:
144 """
145 Returns the value stored under the given *key*.
147 The key must exist.
148 """
149 return self.session[key]
151 def __ior__(self, other: dict):
152 """
153 Merges the contents of a dict into the session.
154 """
155 self.session |= other
156 return self
158 def get(self, key: str, default: t.Any = None) -> t.Any:
159 """
160 Returns the value stored under the given key.
162 :param key: Key to retrieve from the session variables.
163 :param default: Default value to return when key does not exist.
164 """
165 return self.session.get(key, default)
167 def __setitem__(self, key: str, item: t.Any):
168 """
169 Stores a new value under the given key.
171 If that key exists before, its value is
172 overwritten.
173 """
174 self.session[key] = item
175 self.changed = True
177 def markChanged(self) -> None:
178 """
179 Explicitly mark the current session as changed.
180 This will force save() to write into the datastore,
181 even if it believes that this session hasn't changed.
182 """
183 self.changed = True
185 def reset(self) -> None:
186 """
187 Invalidates the current session and starts a new one.
189 This function is especially useful at login, where
190 we might need to create an SSL-capable session.
192 :warning: Everything is flushed.
193 """
194 if self.cookie_key:
195 db.Delete(db.Key(self.kindName, self.cookie_key))
196 from viur.core import securitykey
197 securitykey.clear_session_skeys(self.cookie_key)
199 self.cookie_key = utils.string.random(42)
200 self.static_security_key = utils.string.random(13)
201 self.session.clear()
202 self.changed = True
204 def items(self) -> 'dict_items':
205 """
206 Returns all items in the current session.
207 """
208 return self.session.items()
211@tasks.CallDeferred
212def killSessionByUser(user: t.Optional[t.Union[str, "db.Key", None]] = None):
213 """
214 Invalidates all active sessions for the given *user*.
216 This means that this user is instantly logged out.
217 If no user is given, it tries to invalidate **all** active sessions.
219 Use "__guest__" to kill all sessions not associated with a user.
221 :param user: UserID, "__guest__" or None.
222 """
223 logging.info(f"Invalidating all sessions for {user=}")
225 query = db.Query(Session.kindName).filter("user =", str(user))
226 for obj in query.iter():
227 db.Delete(obj.key)
230@tasks.PeriodicTask(60 * 4)
231def start_clear_sessions():
232 """
233 Removes old (expired) Sessions
234 """
235 query = db.Query(Session.kindName).filter("lastseen <", time.time() - (conf.user.session_life_time + 300))
236 DeleteEntitiesIter.startIterOnQuery(query)