Coverage for gws-app/gws/plugin/auth_session_manager/sqlite/__init__.py: 97%

64 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""SQLite session manager. 

2 

3Stores sessions in the table ``sessions`` of an SQLite database. By default, 

4the database file is ``sessions.<version>.sqlite`` in the GWS misc directory, 

5where ``<version>`` is the GWS version without the patch number. The table is 

6created on first use. 

7 

8A row holds the session uid (a random 64-character string), the uids of the 

9method and the user, the serialized user, the session data as JSON, and the 

10creation and update times. ``cleanup`` deletes expired sessions; ``create`` 

11runs it at most every 10 minutes. ``touch`` updates the update time at most 

12once a minute, unless the session data has changed. A session whose user 

13cannot be restored is deleted and returned with the guest user. 

14 

15Example:: 

16 

17 auth.session { 

18 type "sqlite" 

19 path "/data/sessions.sqlite" 

20 lifeTime "30m" 

21 maxLifeTime "8h" 

22 } 

23""" 

24 

25from typing import Optional 

26 

27import gws 

28import gws.base.auth 

29import gws.lib.datetimex as dtx 

30import gws.lib.jsonx 

31import gws.lib.sqlitex 

32 

33 

34_CLEANUP_INTERVAL = 600 

35_TOUCH_INTERVAL = 60 

36 

37 

38@gws.ext.config.authSessionManager('sqlite') 

39class Config(gws.base.auth.session_manager.Config): 

40 """Session storage in an SQLite database.""" 

41 

42 path: Optional[str] 

43 """Path to the SQLite session database file.""" 

44 

45 

46@gws.ext.object.authSessionManager('sqlite') 

47class Object(gws.base.auth.session_manager.Object): 

48 """SQLite session manager.""" 

49 

50 dbPath: str 

51 """Path to the SQLite database file.""" 

52 table = 'sessions' 

53 

54 def configure(self): 

55 ver = self.root.specs.version.rpartition('.')[0] 

56 self.dbPath = self.cfg('path', default=f'{gws.c.MISC_DIR}/sessions.{ver}.sqlite') 

57 

58 ## 

59 

60 _cleanupTime = 0 

61 

62 def cleanup(self): 

63 self._db().execute( 

64 f'DELETE FROM {self.table} WHERE updated < :last_time OR created < :max_time', 

65 last_time=gws.u.stime() - self.lifeTime, 

66 max_time=(gws.u.stime() - self.maxLifeTime) if self.maxLifeTime > 0 else 0, 

67 ) 

68 self._cleanupTime = gws.u.stime() 

69 

70 def create(self, method, user, data=None): 

71 if gws.u.stime() > self._cleanupTime + _CLEANUP_INTERVAL: 

72 self.cleanup() 

73 

74 am = self.root.app.authMgr 

75 uid = gws.u.random_string(64) 

76 

77 self._db().insert(self.table, dict( 

78 uid=uid, 

79 method_uid=method.uid, 

80 user_uid=user.uid, 

81 str_user=am.serialize_user(user), 

82 str_data=gws.lib.jsonx.to_string(data or {}), 

83 created=gws.u.stime(), 

84 updated=gws.u.stime(), 

85 )) 

86 

87 return self.get(uid) 

88 

89 def delete(self, sess): 

90 self._db().execute(f'DELETE FROM {self.table} WHERE uid=:uid', uid=sess.uid) 

91 

92 def delete_all(self): 

93 self._db().execute(f'DELETE FROM {self.table}') 

94 

95 def get(self, uid): 

96 rs = self._db().select( 

97 f'SELECT * FROM {self.table} WHERE uid=:uid AND updated >= :last_time AND created >= :max_time', 

98 uid=uid, 

99 last_time=gws.u.stime() - self.lifeTime, 

100 max_time=(gws.u.stime() - self.maxLifeTime) if self.maxLifeTime > 0 else 0, 

101 ) 

102 if len(rs) == 1: 

103 return self._session(rs[0]) 

104 

105 def list_all(self): 

106 return [ 

107 self._session(rec) 

108 for rec in self._db().select(f'SELECT * FROM {self.table}') 

109 ] 

110 

111 def save(self, sess): 

112 if not sess.isChanged: 

113 return 

114 

115 self._db().execute( 

116 f'UPDATE {self.table} SET str_data=:str_data, updated=:updated WHERE uid=:uid', 

117 str_data=gws.lib.jsonx.to_string(sess.data or {}), 

118 updated=gws.u.stime(), 

119 uid=sess.uid 

120 ) 

121 

122 sess.isChanged = False 

123 

124 def touch(self, sess): 

125 if sess.isChanged: 

126 return self.save(sess) 

127 

128 # throttle updates to avoid excessive writes on high-traffic sessions 

129 if gws.u.stime() - dtx.to_timestamp(sess.updated) < _TOUCH_INTERVAL: 

130 return 

131 

132 self._db().execute( 

133 f'UPDATE {self.table} SET updated=:updated WHERE uid=:uid', 

134 updated=gws.u.stime(), 

135 uid=sess.uid 

136 ) 

137 

138 ## 

139 

140 def _session(self, rec): 

141 """Create a session object from a database row.""" 

142 am = self.root.app.authMgr 

143 r = gws.u.to_dict(rec) 

144 usr = am.unserialize_user(r['str_user']) 

145 if not usr: 

146 gws.log.error(f'invalid user in session {r["uid"]!r}') 

147 self._db().execute(f'DELETE FROM {self.table} WHERE uid=:uid', uid=r['uid']) 

148 usr = am.guestUser 

149 return gws.base.auth.session.Object( 

150 uid=r['uid'], 

151 method=am.get_method(r['method_uid']), 

152 user=usr, 

153 data=gws.lib.jsonx.from_string(r['str_data']), 

154 created=dtx.from_timestamp(r['created']), 

155 updated=dtx.from_timestamp(r['updated']), 

156 ) 

157 

158 ## 

159 

160 _sqlitex: gws.lib.sqlitex.Object 

161 

162 def _db(self): 

163 """Return the database object, creating the table if needed.""" 

164 if getattr(self, '_sqlitex', None) is None: 

165 ddl = f''' 

166 CREATE TABLE IF NOT EXISTS {self.table} ( 

167 uid TEXT NOT NULL PRIMARY KEY, 

168 method_uid TEXT NOT NULL, 

169 user_uid TEXT NOT NULL, 

170 str_user TEXT NOT NULL, 

171 str_data TEXT NOT NULL, 

172 created INTEGER NOT NULL, 

173 updated INTEGER NOT NULL 

174 ) 

175 ''' 

176 self._sqlitex = gws.lib.sqlitex.Object(self.dbPath, ddl) 

177 return self._sqlitex