0001-misc-use-database-to-store-form-tokens-71455.patch
tests/test_sessions.py | ||
---|---|---|
239 | 239 |
resp = resp.form.submit('submit') |
240 | 240 | |
241 | 241 |
assert sql.Session.count() == 1 |
242 |
assert sql.TransientData.count() == 1 |
|
243 |
transient_data = sql.TransientData.select()[0] |
|
242 |
assert sql.TransientData.count() == 2 |
|
243 |
transient_data1, transient_data2 = sql.TransientData.select(order_by='last_update_time') |
|
244 |
assert transient_data1.data is None # form_token |
|
245 |
assert transient_data2.data == {'1': 'test'} # magictoken |
|
244 | 246 | |
245 | 247 |
app.get('/logout') |
246 | 248 |
assert sql.Session.count() == 0 |
247 | 249 |
assert sql.TransientData.count() == 0 |
248 | 250 | |
249 |
transient_data.store() # session_id not found, should not fail |
|
251 |
transient_data2.store() # session_id not found, should not fail
|
|
250 | 252 | |
251 | 253 | |
252 | 254 |
def test_magictoken_migration(pub, app): |
tests/test_sql.py | ||
---|---|---|
1 | 1 |
import datetime |
2 | 2 |
import io |
3 |
import os |
|
3 | 4 |
import pickle |
4 | 5 |
import random |
5 | 6 |
import string |
... | ... | |
2449 | 2450 |
# check it's no longer needed afterwards |
2450 | 2451 |
sql.migrate() |
2451 | 2452 |
assert sql.is_reindex_needed('python_ds_migration', conn=conn, cur=cur) is False |
2453 | ||
2454 | ||
2455 |
def test_form_tokens_migration(pub): |
|
2456 |
conn, cur = sql.get_connection_and_cursor() |
|
2457 |
cur.execute('UPDATE wcs_meta SET value = 70 WHERE key = %s', ('sql_level',)) |
|
2458 |
conn.commit() |
|
2459 |
cur.close() |
|
2460 | ||
2461 |
form_tokens_dir = os.path.join(pub.app_dir, 'form_tokens') |
|
2462 |
if not os.path.exists(form_tokens_dir): |
|
2463 |
os.mkdir(form_tokens_dir) |
|
2464 |
with open(os.path.join(form_tokens_dir, '1234'), 'w'): |
|
2465 |
pass |
|
2466 | ||
2467 |
assert os.path.exists(os.path.join(form_tokens_dir, '1234')) |
|
2468 |
sql.migrate() |
|
2469 |
assert not os.path.exists(os.path.join(form_tokens_dir, '1234')) |
|
2470 |
assert not os.path.exists(form_tokens_dir) |
wcs/qommon/publisher.py | ||
---|---|---|
130 | 130 |
app_dir = None |
131 | 131 |
_i18n_catalog = None |
132 | 132 | |
133 |
@property |
|
134 |
def form_tokens_dir(self): |
|
135 |
return os.path.join(self.app_dir, 'form_tokens') |
|
136 | ||
137 | 133 |
def get_root_url(self): |
138 | 134 |
if self.get_request(): |
139 | 135 |
return self.get_request().environ['SCRIPT_NAME'] + '/' |
... | ... | |
478 | 474 |
raise ImmediateRedirectException(self.missing_appdir_redirect) |
479 | 475 |
raise Http404() |
480 | 476 | |
481 |
try: |
|
482 |
os.mkdir(self.form_tokens_dir) |
|
483 |
except OSError: # already exists |
|
484 |
pass |
|
485 | ||
486 | 477 |
def init_publish(self, request): |
487 | 478 |
self.set_app_dir(request) |
488 | 479 | |
... | ... | |
614 | 605 |
except KeyError: |
615 | 606 |
pass |
616 | 607 |
continue |
617 |
# also delete obsolete form_tokens that would have be missed when |
|
618 |
# cleaning sessions. |
|
619 |
form_tokens_dir = self.form_tokens_dir |
|
620 |
if os.path.exists(form_tokens_dir): |
|
621 |
for filename in os.listdir(form_tokens_dir): |
|
622 |
try: |
|
623 |
if os.stat(os.path.join(form_tokens_dir, filename)).st_mtime < creation_limit: |
|
624 |
os.unlink(os.path.join(form_tokens_dir, filename)) |
|
625 |
except FileNotFoundError: |
|
626 |
pass |
|
627 | 608 |
except locket.LockError: |
628 | 609 |
pass |
629 | 610 |
wcs/qommon/sessions.py | ||
---|---|---|
166 | 166 | |
167 | 167 |
session_id = property(get_session_id, set_session_id) |
168 | 168 | |
169 |
def get_form_token_filepath(self, token): |
|
170 |
return os.path.join(get_publisher().form_tokens_dir, token) |
|
171 | ||
172 |
def create_form_token(self): |
|
173 |
token = super().create_form_token() |
|
174 |
with open(self.get_form_token_filepath(token), 'wb'): |
|
175 |
# create empty file |
|
176 |
pass |
|
177 |
return token |
|
178 | ||
179 |
def has_form_token(self, token): |
|
180 |
if not token: |
|
181 |
return False |
|
182 |
has_form_token = super().has_form_token(token) |
|
183 |
if not os.path.exists(self.get_form_token_filepath(token)): |
|
184 |
has_form_token = False |
|
185 |
return has_form_token |
|
186 | ||
187 |
def remove_form_token(self, token): |
|
188 |
super().remove_form_token(token) |
|
189 |
self.store() |
|
190 |
try: |
|
191 |
os.unlink(self.get_form_token_filepath(token)) |
|
192 |
except OSError: |
|
193 |
pass |
|
194 | ||
195 |
def clean_form_tokens(self): |
|
196 |
dirname = os.path.join(get_publisher().app_dir, 'form_tokens') |
|
197 |
for token in self._form_tokens: |
|
198 |
try: |
|
199 |
os.unlink(os.path.join(dirname, token)) |
|
200 |
except OSError: |
|
201 |
pass |
|
202 | ||
203 | 169 |
def has_user(self): |
204 | 170 |
user_id = QuixoteSession.get_user(self) |
205 | 171 |
return bool(user_id) |
... | ... | |
400 | 366 |
def __delitem__(self, session_id): |
401 | 367 |
if not session_id: |
402 | 368 |
return |
403 |
try: |
|
404 |
session = self.session_class.get(session_id) |
|
405 |
except KeyError: |
|
406 |
session = None |
|
407 |
try: |
|
408 |
self.session_class.remove_object(session_id) |
|
409 |
except OSError: |
|
410 |
raise KeyError |
|
411 | ||
412 |
if session: |
|
413 |
session.clean_form_tokens() |
|
369 |
self.session_class.remove_object(session_id) |
|
414 | 370 | |
415 | 371 |
def get_sessions_for_saml(self, name_identifier=Ellipsis, session_indexes=()): |
416 | 372 |
return self.session_class.get_sessions_for_saml(name_identifier, session_indexes) |
wcs/sql.py | ||
---|---|---|
19 | 19 |
import io |
20 | 20 |
import itertools |
21 | 21 |
import json |
22 |
import os |
|
22 | 23 |
import re |
24 |
import secrets |
|
25 |
import shutil |
|
23 | 26 |
import time |
24 | 27 | |
25 | 28 |
import psycopg2 |
... | ... | |
3518 | 3521 | |
3519 | 3522 | |
3520 | 3523 |
class TransientData(SqlMixin): |
3521 |
# table to keep some transient submission data out of global session dictionary
|
|
3524 |
# table to keep some transient submission data and form tokens out of session object
|
|
3522 | 3525 |
_table_name = 'transient_data' |
3523 | 3526 |
_table_static_fields = [ |
3524 | 3527 |
('id', 'varchar'), |
... | ... | |
3538 | 3541 |
sql_dict = { |
3539 | 3542 |
'id': self.id, |
3540 | 3543 |
'session_id': self.session_id, |
3541 |
'data': bytearray(pickle.dumps(self.data, protocol=2)), |
|
3544 |
'data': bytearray(pickle.dumps(self.data, protocol=2)) if self.data is not None else None,
|
|
3542 | 3545 |
'last_update_time': now(), |
3543 | 3546 |
} |
3544 | 3547 | |
... | ... | |
3565 | 3568 |
o = cls.__new__(cls) |
3566 | 3569 |
o.id = str_encode(row[0]) |
3567 | 3570 |
o.session_id = row[1] |
3568 |
o.data = pickle_loads(row[2]) |
|
3571 |
o.data = pickle_loads(row[2]) if row[2] else None
|
|
3569 | 3572 |
return o |
3570 | 3573 | |
3571 | 3574 |
@classmethod |
... | ... | |
3720 | 3723 |
super().remove_magictoken(token) |
3721 | 3724 |
TransientData.remove_object(token) |
3722 | 3725 | |
3726 |
def create_form_token(self): |
|
3727 |
token = TransientData(id=secrets.token_urlsafe(16), session_id=self.id, data=None) |
|
3728 |
token.store() |
|
3729 |
return token.id |
|
3730 | ||
3731 |
def has_form_token(self, token): |
|
3732 |
return TransientData.exists([Equal('id', token)]) |
|
3733 | ||
3734 |
def remove_form_token(self, token): |
|
3735 |
TransientData.remove_object(token) |
|
3736 | ||
3723 | 3737 | |
3724 | 3738 |
class TrackingCode(SqlMixin, wcs.tracking_code.TrackingCode): |
3725 | 3739 |
_table_name = 'tracking_codes' |
... | ... | |
4660 | 4674 |
# latest migration, number + description (description is not used |
4661 | 4675 |
# programmaticaly but will make sure git conflicts if two migrations are |
4662 | 4676 |
# separately added with the same number) |
4663 |
SQL_LEVEL = (71, 'python datasource migration')
|
|
4677 |
SQL_LEVEL = (72, 'form tokens to db')
|
|
4664 | 4678 | |
4665 | 4679 | |
4666 | 4680 |
def migrate_global_views(conn, cur): |
... | ... | |
4948 | 4962 |
# 71: python datasource migration |
4949 | 4963 |
set_reindex('python_ds_migration', 'needed', conn=conn, cur=cur) |
4950 | 4964 | |
4965 |
if sql_level < 72: |
|
4966 |
# 72: form tokens to db |
|
4967 |
# it uses the existing tokens table, this "migration" is just to remove old files. |
|
4968 |
form_tokens_dir = os.path.join(get_publisher().app_dir, 'form_tokens') |
|
4969 |
if os.path.exists(form_tokens_dir): |
|
4970 |
shutil.rmtree(form_tokens_dir, ignore_errors=True) |
|
4971 | ||
4951 | 4972 |
if sql_level != SQL_LEVEL[0]: |
4952 | 4973 |
cur.execute( |
4953 | 4974 |
'''UPDATE wcs_meta SET value = %s, updated_at=NOW() WHERE key = %s''', |
4954 |
- |