diff --git a/hc/accounts/tests/test_project.py b/hc/accounts/tests/test_project.py index a2032d58..101d8f56 100644 --- a/hc/accounts/tests/test_project.py +++ b/hc/accounts/tests/test_project.py @@ -1,8 +1,10 @@ from django.core import mail from django.conf import settings +from django.test.utils import override_settings from hc.test import BaseTestCase from hc.accounts.models import Member +from hc.api.models import TokenBucket class ProjectTestCase(BaseTestCase): @@ -83,6 +85,20 @@ class ProjectTestCase(BaseTestCase): " Alice's Project on %s" % settings.SITE_NAME) self.assertEqual(mail.outbox[0].subject, subj) + @override_settings(SECRET_KEY="test-secret") + def test_it_rate_limits_invites(self): + obj = TokenBucket(value="invite-%d" % self.alice.id) + obj.tokens = 0 + obj.save() + + self.client.login(username="alice@example.org", password="password") + + form = {"invite_team_member": "1", "email": "frank@example.org"} + r = self.client.post(self.url, form) + self.assertContains(r, "Too Many Requests") + + self.assertEqual(len(mail.outbox), 0) + def test_it_requires_owner_to_add_team_member(self): self.client.login(username="bob@example.org", password="password") diff --git a/hc/accounts/views.py b/hc/accounts/views.py index f12e9a84..ef2f9602 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -285,6 +285,8 @@ def project(request, code): form = InviteTeamMemberForm(request.POST) if form.is_valid(): + if not TokenBucket.authorize_invite(request.user): + return render(request, "try_later.html") email = form.cleaned_data["email"] try: diff --git a/hc/api/models.py b/hc/api/models.py index 326cb636..d0d3d0da 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -632,7 +632,7 @@ class TokenBucket(models.Model): salted_encoded = (email + settings.SECRET_KEY).encode() value = "em-%s" % hashlib.sha1(salted_encoded).hexdigest() - # 20 emails per 3600 seconds (1 hour): + # 20 login attempts for a single email per hour: return TokenBucket.authorize(value, 20, 3600) @staticmethod @@ -643,5 +643,12 @@ class TokenBucket(models.Model): salted_encoded = (ip + settings.SECRET_KEY).encode() value = "ip-%s" % hashlib.sha1(salted_encoded).hexdigest() - # 20 login attempts from a single IP per 3600 seconds (1 hour): + # 20 login attempts from a single IP per hour: return TokenBucket.authorize(value, 20, 3600) + + @staticmethod + def authorize_invite(user): + value = "invite-%d" % user.id + + # 20 invites per day + return TokenBucket.authorize(value, 20, 3600 * 24)