Re-think rate limiting for resending
authorMagnus Hagander <magnus@hagander.net>
Wed, 19 Jun 2019 19:14:19 +0000 (21:14 +0200)
committerMagnus Hagander <magnus@hagander.net>
Wed, 19 Jun 2019 19:14:19 +0000 (21:14 +0200)
The way it was done ended up defeaeting the service sending things right
away for people who did *not* violate the rate limit.

So instead, keep track of exactly when the last email was sent for each
user, and rate-limit based on that.

django/archives/mailarchives/migrations/0004_resend_rate_limit.py [new file with mode: 0644]
django/archives/mailarchives/models.py
django/archives/mailarchives/views.py
resender/archives_resender.py

diff --git a/django/archives/mailarchives/migrations/0004_resend_rate_limit.py b/django/archives/mailarchives/migrations/0004_resend_rate_limit.py
new file mode 100644 (file)
index 0000000..bdd522f
--- /dev/null
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-06-19 19:02
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth', '0008_alter_user_username_max_length'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('mailarchives', '0003_message_resend'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LastResentMessage',
+            fields=[
+                ('sentto', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+                ('sentat', models.DateTimeField()),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='resendmessage',
+            unique_together=set([('message', 'sendto')]),
+        ),
+    ]
index 5c0c1e03244f4be27e977d76b730536988c7b7ab..0affa3b418ad1d21c841e61521ec78f1eeb6845e 100644 (file)
@@ -126,6 +126,14 @@ class ResendMessage(models.Model):
     sendto = models.ForeignKey(User, null=False, blank=False)
     registeredat = models.DateTimeField(null=False, blank=False)
 
+    class Meta:
+        unique_together = (('message', 'sendto'), )
+
+
+class LastResentMessage(models.Model):
+    sentto = models.ForeignKey(User, null=False, blank=False, primary_key=True)
+    sentat = models.DateTimeField(null=False, blank=False)
+
 
 class ApiClient(models.Model):
     apikey = models.CharField(max_length=100, null=False, blank=False)
index 03fdf7b70990491982a88ca4441bd3c3ab684a42..4835e7e8b3f503351f09a7401a74a5552a3d508f 100644 (file)
@@ -647,10 +647,15 @@ def resend(request, messageid):
     if request.method == 'POST':
         if request.POST.get('resend', None) == '1':
             # Figure out if this user has sent an email recently, and if so refuse it
-            if ResendMessage.objects.filter(sendto=request.user, registeredat__gt=datetime.now()).exists():
+            if LastResentMessage.objects.filter(sentto=request.user, sentat__gt=datetime.now() - timedelta(seconds=settings.RESEND_RATE_LIMIT_SECONDS)).exists():
                 return HttpResponse("You have already requested an email to be sent in the past {0} seconds. Please try again later.".format(settings.RESEND_RATE_LIMIT_SECONDS))
 
-            ResendMessage.objects.get_or_create(message=m, sendto=request.user, registeredat=datetime.now() + timedelta(seconds=settings.RESEND_RATE_LIMIT_SECONDS))
+            ResendMessage.objects.get_or_create(message=m, sendto=request.user, defaults={
+                'registeredat': datetime.now(),
+            })
+            connection.cursor().execute("INSERT INTO mailarchives_lastresentmessage (sentto_id, sentat) VALUES (%(id)s, CURRENT_TIMESTAMP) ON CONFLICT (sentto_id) DO UPDATE SET sentat=EXCLUDED.sentat", {
+                'id': request.user.id,
+            })
             connection.cursor().execute("NOTIFY archives_resend")
             return HttpResponseRedirect('/message-id/resend/{0}/complete'.format(m.messageid))
 
index c097ed7663679c4c519cf1b935c72447a32169b0..061ea16feb00fc3c334989a55d608b00ea791b6b 100755 (executable)
@@ -16,7 +16,7 @@ import psycopg2
 
 def process_queue(conn, sender, smtpserver, heloname):
     with conn.cursor() as curs:
-        curs.execute("SELECT r.id, u.email, m.rawtxt FROM mailarchives_resendmessage r INNER JOIN auth_user u ON u.id=r.sendto_id INNER JOIN messages m ON m.id=r.message_id WHERE registeredat < CURRENT_TIMESTAMP ORDER BY r.id FOR UPDATE OF r LIMIT 1")
+        curs.execute("SELECT r.id, u.email, m.rawtxt FROM mailarchives_resendmessage r INNER JOIN auth_user u ON u.id=r.sendto_id INNER JOIN messages m ON m.id=r.message_id ORDER BY r.id FOR UPDATE OF r LIMIT 1")
         ll = curs.fetchall()
         if len(ll) == 0:
             conn.rollback()