mysociety / alaveteli

Provide a Freedom of Information request system for your jurisdiction
https://alaveteli.org
Other
389 stars 195 forks source link

Generic user content rate limiter #7620

Open garethrees opened 1 year ago

garethrees commented 1 year ago

Extract from https://github.com/mysociety/alaveteli/pull/7602 to be able to apply to any User association records.

Here's a sketch.

diff --git a/app/models/user.rb b/app/models/user.rb
index ceb0eab68..f1653a9bc 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -36,7 +36,42 @@
 #  closed_at                         :datetime
 #  login_token                       :string
 #
+class ContentRateLimiter
+  def self.from_relation(records)
+    new(records.klass, records)
+  end
+
+  def initialize(klass, records)
+    @klass = klass
+    @records = records
+  end
+
+  def exceeded_limit?
+    exceeded_daily_cap? || exceeded_creation_rate?
+  end
+
+  def exceeded_daily_cap?
+    return false unless defined?(klass::MAX_PER_DAY)

+    # OR         (created_at: 24.hours.ago..)
+    records.where(created_at: Time.zone.now.at_beginning_of_day..).size >=
+      klass::MAX_PER_DAY
+  end
+
+  def exceeded_creation_rate?
+    return false unless defined?(klass::CREATION_RATE_INTERVALS)
+
+    klass::CREATION_RATE_INTERVALS.any? do |limit, duration|
+      return false if records.size < limit
+      records.limit(limit).all? { |record| record.created_at > duration.ago }
+    end
+  end
+
+  private
+
+  def records
+    records.reorder(created_at: :desc)
+  end
+end
 class User < ApplicationRecord
   include AlaveteliFeatures::Helpers
   include AlaveteliPro::PhaseCounts
@@ -89,10 +124,17 @@ class User < ApplicationRecord
            -> { order(created_at: :desc) },
            inverse_of: :user,
            dependent: :destroy
+
   has_many :comments,
            -> { order(created_at: :desc) },
            inverse_of: :user,
-           dependent: :destroy
+           dependent: :destroy do
+             def limiter
+               ContentRateLimiter.new(proxy_association.reflection, proxy_association.target)
+            end
+          end
+
+
   has_many :public_body_change_requests,
            -> { order(created_at: :desc) },
            inverse_of: :user,
@@ -451,9 +493,7 @@ def can_make_followup?
   def can_make_comments?
     return false unless active?
     return true if is_admin? || is_pro_admin?
-
-    !exceeded_limit?(:comments) &&
-      !Comment.exceeded_creation_rate?(comments)
+    !comments.limiter.exceeded_limit?
   end

   def can_contact_other_users?
garethrees commented 5 months ago

Rails is getting built-in rate limiting! https://edgeapi.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html#method-i-rate_limit