diff --git a/setup.cfg b/setup.cfg index e93d7b4b..30342d5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ exclude = src/baseframe/static enable-extensions = G accept-encodings = utf-8 classmethod-decorators=classmethod, declared_attr +pytest-fixture-no-parentheses = false [pycodestyle] max-line-length = 88 diff --git a/src/baseframe/forms/form.py b/src/baseframe/forms/form.py index cb0d9ed7..c59537d6 100644 --- a/src/baseframe/forms/form.py +++ b/src/baseframe/forms/form.py @@ -4,9 +4,7 @@ import typing as t import typing_extensions as te -import uuid import warnings -from threading import Lock import wtforms from flask import current_app @@ -15,7 +13,7 @@ from wtforms import Field as WTField from wtforms.utils import unset_value -from ..extensions import __, asset_cache +from ..extensions import __ from ..signals import form_validation_error, form_validation_success from . import ( fields as bfields, @@ -98,40 +96,13 @@ } -_nonce_lock = Lock() - - -def _nonce_cache_key(nonce: str) -> str: - return 'form_nonce/' + nonce - - -def _nonce_validator(form: Form, field: bfields.Field) -> None: - # Check for already-used form nonce - if field.data: - with _nonce_lock: - # nonce_lock prevents parallel requests from attempting to use the same - # nonce in a multi-threaded deployment. As a thread lock, it is ineffective - # in a multi-process deployment, as is typical when using uwsgi or gunicorn. - nonce_cache_key = _nonce_cache_key(field.data) - nonce_cache_hit = asset_cache.get(nonce_cache_key) - if nonce_cache_hit is not None: - raise bvalidators.StopValidation(form.form_nonce_error) - # Mark this nonce as used for 10 seconds - asset_cache.set(_nonce_cache_key(field.data), True, 10) - # Set a new nonce. This is a conscious deviation from the convention for - # validators, which are expected to validate but not modify the data, leaving - # that to filters. However, filters run before validators, and the form nonce - # is nonsense data that will be imminently discarded, so this is okay here. - field.data = field.default() - - class Form(BaseForm): """Form with additional methods.""" __expects__: t.Iterable[str] = () __returns__: t.Iterable[str] = () - form_nonce = bfields.NonceField("Nonce", default=lambda: uuid.uuid4().hex) + form_nonce = bfields.NonceField("Nonce", default='') form_nonce_error = __("This form has already been submitted") def __init_subclass__(cls, **kwargs: t.Any) -> None: diff --git a/tests/baseframe_tests/forms/validators_test.py b/tests/baseframe_tests/forms/validators_test.py index 4ca5b4a2..d11d2091 100644 --- a/tests/baseframe_tests/forms/validators_test.py +++ b/tests/baseframe_tests/forms/validators_test.py @@ -231,45 +231,6 @@ def test_html_snippet_invalid_urls(app, tforms) -> None: assert not tforms.all_urls_form.validate() -@pytest.mark.usefixtures('ctx') -def test_nonce_form_on_success(tforms) -> None: - """A form with a nonce cannot be submitted twice.""" - formdata = MultiDict({field.name: field.data for field in tforms.nonce_form}) - nonce = tforms.nonce_form.form_nonce.data - assert nonce - assert tforms.nonce_form.validate() is True - # Nonce changes on each submit - assert nonce != tforms.nonce_form.form_nonce.data - assert not tforms.nonce_form.form_nonce.errors - # Now restore old form contents - tforms.nonce_form.process(formdata=formdata) - # Second attempt on the same form contents will fail - assert tforms.nonce_form.validate() is False - assert tforms.nonce_form.form_nonce.errors - - -@pytest.mark.usefixtures('ctx') -def test_nonce_form_on_failure(tforms) -> None: - """Form resubmission is not blocked (via the nonce) when validation fails.""" - tforms.emoji_form.process( - formdata=MultiDict( - {'emoji': 'not-emoji', 'form_nonce': tforms.emoji_form.form_nonce.data} - ) - ) - assert tforms.emoji_form.validate() is False - assert not tforms.emoji_form.form_nonce.errors - formdata = MultiDict( - {'emoji': '👍', 'form_nonce': tforms.emoji_form.form_nonce.data} - ) - tforms.emoji_form.process(formdata=formdata) - assert tforms.emoji_form.validate() is True - assert not tforms.emoji_form.form_nonce.errors - # Second attempt on the same form data will fail - tforms.emoji_form.process(formdata) - assert tforms.emoji_form.validate() is False - assert tforms.emoji_form.errors - - @pytest.mark.usefixtures('ctx') def test_no_schemes() -> None: class UrlForm(forms.Form): diff --git a/tests/baseframe_tests/statsd_test.py b/tests/baseframe_tests/statsd_test.py index 9e227456..a87b25e1 100644 --- a/tests/baseframe_tests/statsd_test.py +++ b/tests/baseframe_tests/statsd_test.py @@ -48,9 +48,7 @@ class SimpleForm(forms.Form): "Required", validators=[forms.validators.DataRequired()] ) - f = SimpleForm(meta={'csrf': False}) - del f.form_nonce - return f + return SimpleForm(meta={'csrf': False}) def test_default_config(app, statsd) -> None: