Projet

Général

Profil

0001-misc-use-database-to-store-form-tokens-71455.patch

Frédéric Péters, 26 décembre 2022 15:29

Télécharger (9,2 ko)

Voir les différences:

Subject: [PATCH] misc: use database to store form tokens (#71455)

 tests/test_sessions.py  |  8 ++++---
 tests/test_sql.py       | 19 +++++++++++++++++
 wcs/qommon/publisher.py | 19 -----------------
 wcs/qommon/sessions.py  | 46 +----------------------------------------
 wcs/sql.py              | 29 ++++++++++++++++++++++----
 5 files changed, 50 insertions(+), 71 deletions(-)
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
-