Projet

Général

Profil

0001-views-ratelimit-email-form-views-41489.patch

Valentin Deniaud, 22 avril 2020 11:06

Télécharger (7,16 ko)

Voir les différences:

Subject: [PATCH] views: ratelimit email form views (#41489)

 src/authentic2/app_settings.py |  6 ++++
 src/authentic2/views.py        | 37 +++++++++++++++++++++++
 tests/settings.py              |  3 ++
 tests/test_views.py            | 55 ++++++++++++++++++++++++++++++++++
 4 files changed, 101 insertions(+)
src/authentic2/app_settings.py
314 314
    A2_ACCEPT_EMAIL_AUTHENTICATION=Setting(
315 315
        default=True,
316 316
        definition='Enable authentication by email'),
317
    A2_EMAILS_IP_RATELIMIT=Setting(
318
        default='10/h',
319
        definition='Maximum rate of email sendings triggered by the same IP address.'),
320
    A2_EMAILS_ADDRESS_RATELIMIT=Setting(
321
        default='3/d',
322
        definition='Maximum rate of emails sent to the same email address.'),
317 323
)
318 324

  
319 325
app_settings = AppSettings(default_settings)
src/authentic2/views.py
20 20
import random
21 21
import re
22 22

  
23
from ratelimit.utils import is_ratelimited
24

  
23 25
from django.conf import settings
24 26
from django.shortcuts import render, get_object_or_404
25 27
from django.template.loader import render_to_string
......
654 656
        return ctx
655 657

  
656 658
    def form_valid(self, form):
659
        if is_ratelimited(self.request, key='post:email', group='pw-reset-email',
660
                          rate=app_settings.A2_EMAILS_ADDRESS_RATELIMIT, increment=True):
661
            form.add_error(
662
                'email',
663
                _('Multiple emails have already been sent to this address. Further attempts are '
664
                  'blocked, please check your spam folder or try again later.')
665
            )
666
            return self.form_invalid(form)
667
        if is_ratelimited(self.request, key='ip', group='pw-reset-email',
668
                          rate=app_settings.A2_EMAILS_IP_RATELIMIT, increment=True):
669
            form.add_error(
670
                'email',
671
                _('Multiple password reset attempts have already been made from this IP address. No '
672
                  'further email will be sent, please check your spam folder or try again later.')
673
            )
674
            return self.form_invalid(form)
675

  
657 676
        form.save()
658 677
        self.request.session['reset_email'] = form.cleaned_data['email']
659 678
        return super(PasswordResetView, self).form_valid(form)
......
770 789
    def dispatch(self, request, *args, **kwargs):
771 790
        if not getattr(settings, 'REGISTRATION_OPEN', True):
772 791
            raise Http404('Registration is not open.')
792

  
773 793
        self.token = {}
774 794
        self.ou = get_default_ou()
775 795
        # load pre-filled values
......
787 807
        return super(BaseRegistrationView, self).dispatch(request, *args, **kwargs)
788 808

  
789 809
    def form_valid(self, form):
810
        if is_ratelimited(self.request, key='post:email', group='registration-email',
811
                          rate=app_settings.A2_EMAILS_ADDRESS_RATELIMIT, increment=True):
812
            form.add_error(
813
                'email',
814
                _('Multiple emails have already been sent to this address. Further attempts are '
815
                  'blocked, please check your spam folder or try again later.')
816
            )
817
            return self.form_invalid(form)
818
        if is_ratelimited(self.request, key='ip', group='registration-email',
819
                          rate=app_settings.A2_EMAILS_IP_RATELIMIT, increment=True):
820
            form.add_error(
821
                'email',
822
                _('Multiple registration attempts have already been made from this IP address. No '
823
                  'further email will be sent, please check your spam folder or try again later.')
824
            )
825
            return self.form_invalid(form)
826

  
790 827
        email = form.cleaned_data.pop('email')
791 828
        for field in form.cleaned_data:
792 829
            self.token[field] = form.cleaned_data[field]
tests/settings.py
48 48
TEMPLATES[0]['DIRS'].append('tests/templates')
49 49

  
50 50
SITE_BASE_URL = 'http://localhost'
51

  
52
A2_MAX_EMAILS_PER_IP = None
53
A2_MAX_EMAILS_FOR_ADDRESS = None
tests/test_views.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16
# authentic2
17 17

  
18
import datetime
18 19
from utils import login, logout, get_link_from_mail
19 20
import pytest
20 21

  
......
148 149
    response = app.get(reverse('account_management'))
149 150
    assert response.status_code == 302
150 151
    assert response['Location'] == settings.A2_ACCOUNTS_URL
152

  
153

  
154
@pytest.mark.parametrize('view_name', ['registration_register', 'password_reset'])
155
def test_views_email_ratelimit(app, db, simple_user, settings, mailoutbox, freezer, view_name):
156
    freezer.move_to('2020-01-01')
157
    settings.A2_EMAILS_IP_RATELIMIT = '10/h'
158
    settings.A2_EMAILS_ADDRESS_RATELIMIT = '3/d'
159
    users = [User.objects.create(email='test%s@test.com' % i) for i in range(8)]
160

  
161
    # reach email limit
162
    for _ in range(3):
163
        response = app.get(reverse(view_name))
164
        response.form.set('email', simple_user.email)
165
        response = response.form.submit()
166
    assert len(mailoutbox) == 3
167

  
168
    response = app.get(reverse(view_name))
169
    response.form.set('email', simple_user.email)
170
    response = response.form.submit()
171
    assert len(mailoutbox) == 3
172
    assert 'try again later' in response.text
173

  
174
    # reach ip limit
175
    for i in range(7):
176
        response = app.get(reverse(view_name))
177
        response.form.set('email', users[i].email)
178
        response = response.form.submit()
179
    assert len(mailoutbox) == 10
180

  
181
    response = app.get(reverse(view_name))
182
    response.form.set('email', users[i + 1].email)
183
    response = response.form.submit()
184
    assert len(mailoutbox) == 10
185
    assert 'try again later' in response.text
186

  
187
    # ip ratelimits are lifted after an hour
188
    freezer.tick(datetime.timedelta(hours=1))
189
    response = app.get(reverse(view_name))
190
    response.form.set('email', users[0].email)
191
    response = response.form.submit()
192
    assert len(mailoutbox) == 11
193

  
194
    # email ratelimits are lifted after a day
195
    response = app.get(reverse(view_name))
196
    response.form.set('email', simple_user.email)
197
    response = response.form.submit()
198
    assert len(mailoutbox) == 11
199
    assert 'try again later' in response.text
200

  
201
    freezer.tick(datetime.timedelta(days=1))
202
    response = app.get(reverse(view_name))
203
    response.form.set('email', simple_user.email)
204
    response = response.form.submit()
205
    assert len(mailoutbox) == 12
151
-