[deploy] Rate Limit Feedback Message Creation (#7832)
This commit is contained in:
parent
d2fbad08d4
commit
d1e39a7dba
8 changed files with 54 additions and 19 deletions
|
|
@ -92,5 +92,11 @@ class ApplicationController < ActionController::Base
|
||||||
rate_limiter.check_limit!(action)
|
rate_limiter.check_limit!(action)
|
||||||
end
|
end
|
||||||
|
|
||||||
delegate :rate_limiter, to: :current_user
|
def rate_limiter
|
||||||
|
(current_user || anonymous_user).rate_limiter
|
||||||
|
end
|
||||||
|
|
||||||
|
def anonymous_user
|
||||||
|
User.new(ip_address: request.env["HTTP_FASTLY_CLIENT_IP"])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ class FeedbackMessagesController < ApplicationController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
flash.clear
|
flash.clear
|
||||||
|
rate_limit!(:feedback_message_creation)
|
||||||
|
|
||||||
params = feedback_message_params.merge(reporter_id: current_user&.id)
|
params = feedback_message_params.merge(reporter_id: current_user&.id)
|
||||||
@feedback_message = FeedbackMessage.new(params)
|
@feedback_message = FeedbackMessage.new(params)
|
||||||
|
|
@ -16,6 +17,7 @@ class FeedbackMessagesController < ApplicationController
|
||||||
reported_url: feedback_message_params[:reported_url],
|
reported_url: feedback_message_params[:reported_url],
|
||||||
message: feedback_message_params[:message],
|
message: feedback_message_params[:message],
|
||||||
)
|
)
|
||||||
|
rate_limiter.track_limit_by_action(:feedback_message_creation)
|
||||||
|
|
||||||
redirect_to feedback_messages_path
|
redirect_to feedback_messages_path
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ module RateLimitCheckerHelper
|
||||||
placeholder: 150,
|
placeholder: 150,
|
||||||
description: "The number of article updates a user can make in 30 seconds"
|
description: "The number of article updates a user can make in 30 seconds"
|
||||||
},
|
},
|
||||||
|
rate_limit_feedback_message_creation: {
|
||||||
|
min: 1,
|
||||||
|
placeholder: 5,
|
||||||
|
description: "The number of times a user can submit feedback in a 5 minute period"
|
||||||
|
},
|
||||||
rate_limit_follow_count_daily: {
|
rate_limit_follow_count_daily: {
|
||||||
min: 0,
|
min: 0,
|
||||||
placeholder: 500,
|
placeholder: 500,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ class SiteConfig < RailsSettings::Base
|
||||||
field :rate_limit_email_recipient, type: :integer, default: 5
|
field :rate_limit_email_recipient, type: :integer, default: 5
|
||||||
field :rate_limit_article_update, type: :integer, default: 150
|
field :rate_limit_article_update, type: :integer, default: 150
|
||||||
field :rate_limit_send_email_confirmation, type: :integer, default: 2
|
field :rate_limit_send_email_confirmation, type: :integer, default: 2
|
||||||
|
field :rate_limit_feedback_message_creation, type: :integer, default: 5
|
||||||
|
|
||||||
# Google Analytics Reporting API v4
|
# Google Analytics Reporting API v4
|
||||||
# <https://developers.google.com/analytics/devguides/reporting/core/v4>
|
# <https://developers.google.com/analytics/devguides/reporting/core/v4>
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,9 @@ class User < ApplicationRecord
|
||||||
reserved_username: "username is reserved"
|
reserved_username: "username is reserved"
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
attr_accessor(
|
attr_accessor :scholar_email, :new_note, :note_for_current_role, :user_status, :pro, :merge_user_id,
|
||||||
:scholar_email, :new_note, :note_for_current_role, :user_status, :pro, :merge_user_id,
|
:add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ghostify,
|
||||||
:add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ghostify
|
:ip_address
|
||||||
)
|
|
||||||
|
|
||||||
rolify after_add: :index_roles, after_remove: :index_roles
|
rolify after_add: :index_roles, after_remove: :index_roles
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ class RateLimitChecker
|
||||||
ACTION_LIMITERS = {
|
ACTION_LIMITERS = {
|
||||||
article_update: { retry_after: 30 },
|
article_update: { retry_after: 30 },
|
||||||
send_email_confirmation: { retry_after: 120 },
|
send_email_confirmation: { retry_after: 120 },
|
||||||
|
feedback_message_creation: { retry_after: 300 },
|
||||||
image_upload: { retry_after: 30 },
|
image_upload: { retry_after: 30 },
|
||||||
listing_creation: { retry_after: 60 },
|
listing_creation: { retry_after: 60 },
|
||||||
published_article_creation: { retry_after: 30 },
|
published_article_creation: { retry_after: 30 },
|
||||||
|
|
@ -66,9 +67,10 @@ class RateLimitChecker
|
||||||
end
|
end
|
||||||
|
|
||||||
def limit_cache_key(action)
|
def limit_cache_key(action)
|
||||||
raise "Invalid Cache Key: user ID can't be blank" unless @user.id
|
unique_key_component = @user&.id || @user&.ip_address
|
||||||
|
raise "Invalid Cache Key: no unique component present" if unique_key_component.blank?
|
||||||
|
|
||||||
"#{@user.id}_#{action}"
|
"#{unique_key_component}_#{action}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_rate_limit(action)
|
def action_rate_limit(action)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ RSpec.describe "feedback_messages", type: :request do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers = { "HTTP_FASTLY_CLIENT_IP" => "5.6.7.8" }
|
||||||
|
|
||||||
context "with valid params" do
|
context "with valid params" do
|
||||||
before do
|
before do
|
||||||
mock_recaptcha_verification
|
mock_recaptcha_verification
|
||||||
|
|
@ -26,7 +28,7 @@ RSpec.describe "feedback_messages", type: :request do
|
||||||
|
|
||||||
it "creates a feedback message" do
|
it "creates a feedback message" do
|
||||||
expect do
|
expect do
|
||||||
post feedback_messages_path, params: valid_abuse_report_params
|
post feedback_messages_path, params: valid_abuse_report_params, headers: headers
|
||||||
end.to change(FeedbackMessage, :count).by(1)
|
end.to change(FeedbackMessage, :count).by(1)
|
||||||
|
|
||||||
feedback_message = FeedbackMessage.last
|
feedback_message = FeedbackMessage.last
|
||||||
|
|
@ -37,24 +39,36 @@ RSpec.describe "feedback_messages", type: :request do
|
||||||
|
|
||||||
it "queues a slack message to be sent" do
|
it "queues a slack message to be sent" do
|
||||||
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
|
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
|
||||||
post feedback_messages_path, params: valid_abuse_report_params
|
post feedback_messages_path, params: valid_abuse_report_params, headers: headers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with invalid recaptcha" do
|
context "with invalid recaptcha" do
|
||||||
it "rerenders page" do
|
it "rerenders page" do
|
||||||
post "/feedback_messages", params: valid_abuse_report_params
|
post "/feedback_messages", params: valid_abuse_report_params, headers: headers
|
||||||
expect(response.body).to include("Make sure the forms are filled")
|
expect(response.body).to include("Make sure the forms are filled")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "queues a slack message to be sent" do
|
it "queues a slack message to be sent" do
|
||||||
sidekiq_assert_no_enqueued_jobs(only: Slack::Messengers::Worker) do
|
sidekiq_assert_no_enqueued_jobs(only: Slack::Messengers::Worker) do
|
||||||
post feedback_messages_path, params: valid_abuse_report_params
|
post feedback_messages_path, params: valid_abuse_report_params, headers: headers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when rate limit is reached" do
|
||||||
|
it "returns a 429" do
|
||||||
|
user = create(:user)
|
||||||
|
limiter = user.rate_limiter
|
||||||
|
allow(RateLimitChecker).to receive(:new) { limiter }
|
||||||
|
allow(limiter).to receive(:limit_by_action).and_return(true)
|
||||||
|
|
||||||
|
post "/feedback_messages", params: valid_abuse_report_params, headers: headers
|
||||||
|
expect(response.status).to eq(429)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when a user submits a report" do
|
context "when a user submits a report" do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
|
@ -65,14 +79,14 @@ RSpec.describe "feedback_messages", type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates a feedback message reported by the user" do
|
it "creates a feedback message reported by the user" do
|
||||||
post feedback_messages_path, params: valid_abuse_report_params
|
post feedback_messages_path, params: valid_abuse_report_params, headers: headers
|
||||||
|
|
||||||
expect(FeedbackMessage.exists?(reporter_id: user.id)).to be(true)
|
expect(FeedbackMessage.exists?(reporter_id: user.id)).to be(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "queues a slack message to be sent" do
|
it "queues a slack message to be sent" do
|
||||||
sidekiq_assert_enqueued_jobs(1, only: Slack::Messengers::Worker) do
|
sidekiq_assert_enqueued_jobs(1, only: Slack::Messengers::Worker) do
|
||||||
post feedback_messages_path, params: valid_abuse_report_params
|
post feedback_messages_path, params: valid_abuse_report_params, headers: headers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -83,25 +97,25 @@ RSpec.describe "feedback_messages", type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not add any user as the reporter" do
|
it "does not add any user as the reporter" do
|
||||||
post "/feedback_messages", params: valid_abuse_report_params
|
post "/feedback_messages", params: valid_abuse_report_params, headers: headers
|
||||||
|
|
||||||
expect(FeedbackMessage.last.reporter).to be(nil)
|
expect(FeedbackMessage.last.reporter).to be(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "queues a slack message to be sent" do
|
it "queues a slack message to be sent" do
|
||||||
sidekiq_assert_enqueued_jobs(1, only: Slack::Messengers::Worker) do
|
sidekiq_assert_enqueued_jobs(1, only: Slack::Messengers::Worker) do
|
||||||
post feedback_messages_path, params: valid_abuse_report_params
|
post feedback_messages_path, params: valid_abuse_report_params, headers: headers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects to the index page" do
|
it "redirects to the index page" do
|
||||||
post "/feedback_messages", params: valid_abuse_report_params
|
post "/feedback_messages", params: valid_abuse_report_params, headers: headers
|
||||||
|
|
||||||
expect(response).to redirect_to(feedback_messages_path)
|
expect(response).to redirect_to(feedback_messages_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects and continues to the index page with the correct message" do
|
it "redirects and continues to the index page with the correct message" do
|
||||||
post "/feedback_messages", params: valid_abuse_report_params
|
post "/feedback_messages", params: valid_abuse_report_params, headers: headers
|
||||||
|
|
||||||
follow_redirect!
|
follow_redirect!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,16 @@ RSpec.describe RateLimitChecker, type: :service do
|
||||||
expect(rate_limit_checker.limit_by_action("random-nothing")).to be(false)
|
expect(rate_limit_checker.limit_by_action("random-nothing")).to be(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises an error if user does not have an ID" do
|
it "will limit action by ip_address if present" do
|
||||||
|
action = described_class::ACTION_LIMITERS.keys.first
|
||||||
|
limiter = described_class.new(build(:user, ip_address: "1.1.1.1"))
|
||||||
|
expect { limiter.limit_by_action(action) }.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if no unique component is present for a cache key" do
|
||||||
action = described_class::ACTION_LIMITERS.keys.first
|
action = described_class::ACTION_LIMITERS.keys.first
|
||||||
limiter = described_class.new(build(:user))
|
limiter = described_class.new(build(:user))
|
||||||
expect { limiter.limit_by_action(action) }.to raise_error("Invalid Cache Key: user ID can't be blank")
|
expect { limiter.limit_by_action(action) }.to raise_error("Invalid Cache Key: no unique component present")
|
||||||
end
|
end
|
||||||
|
|
||||||
# published_article_creation limit we check against the database rather than our cache
|
# published_article_creation limit we check against the database rather than our cache
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue