Projet

Général

Profil

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

Frédéric Péters, 08 décembre 2022 22:05

Télécharger (7,08 ko)

Voir les différences:

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

 tests/test_sessions.py  |  8 ++++---
 wcs/qommon/publisher.py | 19 -----------------
 wcs/qommon/sessions.py  | 46 +----------------------------------------
 wcs/sql.py              | 18 +++++++++++++---
 4 files changed, 21 insertions(+), 70 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):
wcs/qommon/publisher.py
131 131
    app_dir = None
132 132
    _i18n_catalog = None
133 133

  
134
    @property
135
    def form_tokens_dir(self):
136
        return os.path.join(self.app_dir, 'form_tokens')
137

  
138 134
    def get_root_url(self):
139 135
        if self.get_request():
140 136
            return self.get_request().environ['SCRIPT_NAME'] + '/'
......
479 475
                raise ImmediateRedirectException(self.missing_appdir_redirect)
480 476
            raise Http404()
481 477

  
482
        try:
483
            os.mkdir(self.form_tokens_dir)
484
        except OSError:  # already exists
485
            pass
486

  
487 478
    def init_publish(self, request):
488 479
        self.set_app_dir(request)
489 480

  
......
615 606
                        except KeyError:
616 607
                            pass
617 608
                        continue
618
                # also delete obsolete form_tokens that would have be missed when
619
                # cleaning sessions.
620
                form_tokens_dir = self.form_tokens_dir
621
                if os.path.exists(form_tokens_dir):
622
                    for filename in os.listdir(form_tokens_dir):
623
                        try:
624
                            if os.stat(os.path.join(form_tokens_dir, filename)).st_mtime < creation_limit:
625
                                os.unlink(os.path.join(form_tokens_dir, filename))
626
                        except FileNotFoundError:
627
                            pass
628 609
        except locket.LockError:
629 610
            pass
630 611

  
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
20 20
import itertools
21 21
import json
22 22
import re
23
import secrets
23 24
import time
24 25

  
25 26
import psycopg2
......
3518 3519

  
3519 3520

  
3520 3521
class TransientData(SqlMixin):
3521
    # table to keep some transient submission data out of global session dictionary
3522
    # table to keep some transient submission data and form tokens out of global session dictionary
3522 3523
    _table_name = 'transient_data'
3523 3524
    _table_static_fields = [
3524 3525
        ('id', 'varchar'),
......
3538 3539
        sql_dict = {
3539 3540
            'id': self.id,
3540 3541
            'session_id': self.session_id,
3541
            'data': bytearray(pickle.dumps(self.data, protocol=2)),
3542
            'data': bytearray(pickle.dumps(self.data, protocol=2)) if self.data is not None else None,
3542 3543
            'last_update_time': now(),
3543 3544
        }
3544 3545

  
......
3565 3566
        o = cls.__new__(cls)
3566 3567
        o.id = str_encode(row[0])
3567 3568
        o.session_id = row[1]
3568
        o.data = pickle_loads(row[2])
3569
        o.data = pickle_loads(row[2]) if row[2] else None
3569 3570
        return o
3570 3571

  
3571 3572
    @classmethod
......
3720 3721
        super().remove_magictoken(token)
3721 3722
        TransientData.remove_object(token)
3722 3723

  
3724
    def create_form_token(self):
3725
        token = TransientData(id=secrets.token_urlsafe(16), session_id=self.id, data=None)
3726
        token.store()
3727
        return token.id
3728

  
3729
    def has_form_token(self, token):
3730
        return TransientData.exists([Equal('id', token)])
3731

  
3732
    def remove_form_token(self, token):
3733
        TransientData.remove_object(token)
3734

  
3723 3735

  
3724 3736
class TrackingCode(SqlMixin, wcs.tracking_code.TrackingCode):
3725 3737
    _table_name = 'tracking_codes'
3726
-