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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""SQLite session manager.
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.
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.
15Example::
17 auth.session {
18 type "sqlite"
19 path "/data/sessions.sqlite"
20 lifeTime "30m"
21 maxLifeTime "8h"
22 }
23"""
25from typing import Optional
27import gws
28import gws.base.auth
29import gws.lib.datetimex as dtx
30import gws.lib.jsonx
31import gws.lib.sqlitex
34_CLEANUP_INTERVAL = 600
35_TOUCH_INTERVAL = 60
38@gws.ext.config.authSessionManager('sqlite')
39class Config(gws.base.auth.session_manager.Config):
40 """Session storage in an SQLite database."""
42 path: Optional[str]
43 """Path to the SQLite session database file."""
46@gws.ext.object.authSessionManager('sqlite')
47class Object(gws.base.auth.session_manager.Object):
48 """SQLite session manager."""
50 dbPath: str
51 """Path to the SQLite database file."""
52 table = 'sessions'
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')
58 ##
60 _cleanupTime = 0
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()
70 def create(self, method, user, data=None):
71 if gws.u.stime() > self._cleanupTime + _CLEANUP_INTERVAL:
72 self.cleanup()
74 am = self.root.app.authMgr
75 uid = gws.u.random_string(64)
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 ))
87 return self.get(uid)
89 def delete(self, sess):
90 self._db().execute(f'DELETE FROM {self.table} WHERE uid=:uid', uid=sess.uid)
92 def delete_all(self):
93 self._db().execute(f'DELETE FROM {self.table}')
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])
105 def list_all(self):
106 return [
107 self._session(rec)
108 for rec in self._db().select(f'SELECT * FROM {self.table}')
109 ]
111 def save(self, sess):
112 if not sess.isChanged:
113 return
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 )
122 sess.isChanged = False
124 def touch(self, sess):
125 if sess.isChanged:
126 return self.save(sess)
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
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 )
138 ##
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 )
158 ##
160 _sqlitex: gws.lib.sqlitex.Object
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