Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor RateLimitChecker #5521

Merged
merged 8 commits into from Jan 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 27 additions & 21 deletions app/labor/rate_limit_checker.rb
@@ -1,10 +1,6 @@
class RateLimitChecker
attr_reader :user, :action

def self.daily_account_follow_limit
SiteConfig.rate_limit_follow_count_daily
end

def initialize(user = nil)
@user = user
end
Expand All @@ -13,18 +9,9 @@ class UploadRateLimitReached < StandardError; end
class DailyFollowAccountLimitReached < StandardError; end

def limit_by_action(action)
result = case action
citizen428 marked this conversation as resolved.
Show resolved Hide resolved
when "comment_creation"
user.comments.where("created_at > ?", 30.seconds.ago).size > 9
when "published_article_creation"
user.articles.published.where("created_at > ?", 30.seconds.ago).size > 9
when "image_upload"
Rails.cache.read("#{user.id}_image_upload").to_i > 9
when "follow_account"
user_today_follow_count > self.class.daily_account_follow_limit
else
false
end
check_method = "check_#{action}_limit"
result = respond_to?(check_method, true) ? send(check_method) : false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the true as second argument to respond_to? so it also considers private methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL that respond_to? takes a second parameter!


if result
@action = action
ping_admins
Expand All @@ -40,19 +27,38 @@ def track_image_uploads

def limit_by_email_recipient_address(address)
# This is related to the recipient, not the "user" initiator, like in action.
EmailMessage.where(to: address).
where("sent_at > ?", 2.minutes.ago).size > 5
EmailMessage.where(to: address).where("sent_at > ?", 2.minutes.ago).size >
SiteConfig.rate_limit_email_recipient
end

private

def check_comment_creation_limit
user.comments.where("created_at > ?", 30.seconds.ago).size >
SiteConfig.rate_limit_comment_creation
end

def check_published_article_creation_limit
user.articles.published.where("created_at > ?", 30.seconds.ago).size >
SiteConfig.rate_limit_published_article_creation
end

def check_image_upload_limit
Rails.cache.read("#{user.id}_image_upload").to_i >
SiteConfig.rate_limit_image_upload
end

def check_follow_account_limit
user_today_follow_count > SiteConfig.rate_limit_follow_count_daily
end

def ping_admins
RateLimitCheckerWorker.perform_async(user.id, action)
end

private

def user_today_follow_count
following_users_count = user.following_users_count
return following_users_count if following_users_count < self.class.daily_account_follow_limit
return following_users_count if following_users_count < SiteConfig.rate_limit_follow_count_daily

now = Time.zone.now
user.follows.where(created_at: (now.beginning_of_day..now)).size
Expand Down
4 changes: 4 additions & 0 deletions app/models/site_config.rb
Expand Up @@ -20,6 +20,10 @@ class SiteConfig < RailsSettings::Base

# rate limits
field :rate_limit_follow_count_daily, type: :integer, default: 500
field :rate_limit_comment_creation, type: :integer, default: 9
field :rate_limit_published_article_creation, type: :integer, default: 9
field :rate_limit_image_upload, type: :integer, default: 9
Comment on lines +23 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to submit another PR after this to expose these rate limits to /internal/config

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was torn on whether or not to do it in the same PR. In the end my reasoning was that I can finish the refactoring first, since the current version is also non-configurable. Then add the configuration options as separate PR, which has the advantage that the PRs are more limited in scope and therefore easier to review. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that works for me!

field :rate_limit_email_recipient, type: :integer, default: 5

# Google Analytics Reporting API v4
# <https://developers.google.com/analytics/devguides/reporting/core/v4>
Expand Down
84 changes: 45 additions & 39 deletions spec/labor/rate_limit_checker_spec.rb
Expand Up @@ -4,83 +4,89 @@
let(:user) { create(:user) }
let(:article) { create(:article, user_id: user.id) }

describe "self.daily_account_follow_limit " do
it "returns the value set in SiteConfig.rate_limit_follow_count_daily" do
expect(described_class.daily_account_follow_limit).to eq(SiteConfig.rate_limit_follow_count_daily)
end
end

describe "#limit_by_action" do
it "returns false for invalid action" do
expect(described_class.new(user).limit_by_action("random-nothing")).to eq(false)
end
let(:rate_limit_checker) { described_class.new(user) }

it "returns true if too many comments at once" do
create_list(:comment, 10, user_id: user.id, commentable_id: article.id)
expect(described_class.new(user).limit_by_action("comment_creation")).to eq(true)
end

it "triggers ping admin when too many comments" do
allow(RateLimitCheckerWorker).to receive(:perform_async)
create_list(:comment, 10, user_id: user.id, commentable_id: article.id)
described_class.new(user).limit_by_action("comment_creation")
expect(RateLimitCheckerWorker).to have_received(:perform_async).with(user.id, "comment_creation")
it "returns false for invalid action" do
expect(rate_limit_checker.limit_by_action("random-nothing")).to eq(false)
end

it "returns false if allowed comment" do
create_list(:comment, 2, user_id: user.id, commentable_id: article.id)
expect(described_class.new(user).limit_by_action("comment_creation")).to eq(false)
context "when creating comments" do
before do
allow(SiteConfig).to receive(:rate_limit_comment_creation).and_return(1)
end

it "returns true if too many comments at once" do
create_list(:comment, 2, user_id: user.id, commentable_id: article.id)
expect(rate_limit_checker.limit_by_action("comment_creation")).to eq(true)
end

it "triggers ping admin when too many comments" do
allow(RateLimitCheckerWorker).to receive(:perform_async)
create_list(:comment, 2, user_id: user.id, commentable_id: article.id)
rate_limit_checker.limit_by_action("comment_creation")
expect(RateLimitCheckerWorker).to have_received(:perform_async).with(user.id, "comment_creation")
end

it "returns false if allowed comment" do
expect(rate_limit_checker.limit_by_action("comment_creation")).to eq(false)
end
end

it "returns true if too many published articles at once" do
create_list(:article, 10, user_id: user.id, published: true)
expect(described_class.new(user).limit_by_action("published_article_creation")).to eq(true)
allow(SiteConfig).to receive(:rate_limit_published_article_creation).and_return(1)
create_list(:article, 2, user_id: user.id, published: true)
expect(rate_limit_checker.limit_by_action("published_article_creation")).to eq(true)
end

it "returns true if a user has followed more than <daily_limit> accounts today" do
rate_limit_checker = described_class.new(user)

allow(rate_limit_checker).
to receive(:user_today_follow_count).
and_return(described_class.daily_account_follow_limit + 1)
and_return(SiteConfig.rate_limit_follow_count_daily + 1)

expect(rate_limit_checker.limit_by_action("follow_account")).to eq(true)
end

it "returns false if a user's following_users_count is less than <daily_limit>" do
rate_limit_checker = described_class.new(user)

allow(user).
to receive(:following_users_count).
and_return(described_class.daily_account_follow_limit - 1)
and_return(SiteConfig.rate_limit_follow_count_daily - 1)

expect(rate_limit_checker.limit_by_action("follow_account")).to eq(false)
end

it "returns false if a user has followed less than <daily_limit> accounts today" do
rate_limit_checker = described_class.new(user)

allow(rate_limit_checker).
to receive(:user_today_follow_count).
and_return(described_class.daily_account_follow_limit - 1)
and_return(SiteConfig.rate_limit_image_upload + 1)

expect(rate_limit_checker.limit_by_action("follow_account")).to eq(false)
end

it "returns false if published articles comment" do
create_list(:article, 2, user_id: user.id, published: true)
it "returns false if published articles limit has not been reached" do
expect(described_class.new(user).limit_by_action("published_article_creation")).to eq(false)
end

it "returns false if a user uploads too many images" do
allow(rate_limit_checker).
to receive(:track_image_uploads).
and_return(SiteConfig.rate_limit_follow_count_daily - 1)

expect(rate_limit_checker.limit_by_action("image_upload")).to eq(false)
end
end

describe "#limit_by_email_recipient_address" do
it "returns true if too many published articles at once" do
10.times { EmailMessage.create(to: user.email, sent_at: Time.current) }
expect(described_class.new.limit_by_email_recipient_address(user.email)).to eq(true)
before do
allow(SiteConfig).to receive(:rate_limit_email_recipient).and_return(1)
end

it "returns false if published articles comment" do
it "returns true if too many emails are sent to the same recipient" do
2.times { EmailMessage.create(to: user.email, sent_at: Time.current) }
expect(described_class.new.limit_by_email_recipient_address(user.email)).to eq(true)
end

it "returns false if we are below the message limit for this recipient" do
expect(described_class.new.limit_by_email_recipient_address(user.email)).to eq(false)
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/requests/follows_create_spec.rb
Expand Up @@ -22,7 +22,7 @@

allow(rate_limit_checker).
to receive(:user_today_follow_count).
and_return(RateLimitChecker.daily_account_follow_limit + 1)
and_return(SiteConfig.rate_limit_follow_count_daily + 1)

allow(RateLimitChecker).
to receive(:new).
Expand Down