0001-views-ratelimit-email-form-views-41489.patch
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 |
- |