Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/session.py: 31%

89 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-03 13:41 +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 

8 

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() 

12 

13 Example: 

14 

15 .. code-block:: python 

16 

17 from viur.core import current 

18 sessionData = current.session.get() 

19 sessionData["your_key"] = "your_data" 

20 data = sessionData["your_key"] 

21 

22 A get-method is provided for convenience. 

23 It returns None instead of raising an Exception if the key is not found. 

24""" 

25 

26 

27class Session: 

28 """ 

29 Store Sessions inside the datastore. 

30 The behaviour of this module can be customized in the following ways: 

31 

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__" 

52 

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() 

59 

60 def load(self, req): 

61 """ 

62 Initializes the Session. 

63 

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 

73 

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") 

77 

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() 

84 

85 def save(self, req): 

86 """ 

87 Writes the session into the database. 

88 

89 Does nothing, in case the session hasn't been changed in the current request. 

90 """ 

91 if not self.changed: 

92 return 

93 

94 # We will not issue sessions over http anymore 

95 if not (req.isSSLConnection or conf.instance.is_dev_server): 

96 return 

97 

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 

104 

105 dbSession = db.Entity(db.Key(self.kindName, self.cookie_key)) 

106 

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"} 

112 

113 db.Put(dbSession) 

114 

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 ) 

123 

124 req.response.headerlist.append( 

125 ("Set-Cookie", f"{self.cookie_name}={self.cookie_key};{';'.join([f for f in flags if f])}") 

126 ) 

127 

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 

133 

134 def __delitem__(self, key: str) -> None: 

135 """ 

136 Removes a *key* from the session. 

137 

138 This key must exist. 

139 """ 

140 del self.session[key] 

141 self.changed = True 

142 

143 def __getitem__(self, key) -> t.Any: 

144 """ 

145 Returns the value stored under the given *key*. 

146 

147 The key must exist. 

148 """ 

149 return self.session[key] 

150 

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 

157 

158 def get(self, key: str, default: t.Any = None) -> t.Any: 

159 """ 

160 Returns the value stored under the given key. 

161 

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) 

166 

167 def __setitem__(self, key: str, item: t.Any): 

168 """ 

169 Stores a new value under the given key. 

170 

171 If that key exists before, its value is 

172 overwritten. 

173 """ 

174 self.session[key] = item 

175 self.changed = True 

176 

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 

184 

185 def reset(self) -> None: 

186 """ 

187 Invalidates the current session and starts a new one. 

188 

189 This function is especially useful at login, where 

190 we might need to create an SSL-capable session. 

191 

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) 

198 

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 

203 

204 def items(self) -> 'dict_items': 

205 """ 

206 Returns all items in the current session. 

207 """ 

208 return self.session.items() 

209 

210 

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*. 

215 

216 This means that this user is instantly logged out. 

217 If no user is given, it tries to invalidate **all** active sessions. 

218 

219 Use "__guest__" to kill all sessions not associated with a user. 

220 

221 :param user: UserID, "__guest__" or None. 

222 """ 

223 logging.info(f"Invalidating all sessions for {user=}") 

224 

225 query = db.Query(Session.kindName).filter("user =", str(user)) 

226 for obj in query.iter(): 

227 db.Delete(obj.key) 

228 

229 

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)