[deploy] Rate Limit Organization Creation (#7595)

This commit is contained in:
Molly Struve 2020-04-30 10:55:37 -05:00 committed by GitHub
parent 1d6c1fe445
commit 74e75b4067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 85 additions and 7 deletions

View file

@ -56,6 +56,7 @@ class Internal::ConfigsController < Internal::ApplicationController
rate_limit_follow_count_daily
rate_limit_image_upload
rate_limit_published_article_creation
rate_limit_organization_creation
shop_url
sidebar_tags
suggested_tags

View file

@ -3,6 +3,8 @@ class OrganizationsController < ApplicationController
rescue_from Errno::ENAMETOOLONG, with: :log_image_data_to_datadog
def create
rate_limit!
@tab = "organization"
@user = current_user
@tab_list = @user.settings_tab_list
@ -15,6 +17,7 @@ class OrganizationsController < ApplicationController
@organization = Organization.new(organization_params)
authorize @organization
if @organization.save
rate_limiter.track_organization_creation
@organization_membership = OrganizationMembership.create!(organization_id: @organization.id, user_id: current_user.id, type_of_user: "admin")
flash[:settings_notice] = "Your organization was successfully created and you are an admin."
redirect_to "/settings/organization/#{@organization.id}"
@ -109,4 +112,17 @@ class OrganizationsController < ApplicationController
@organization.errors.add(:profile_image, FILENAME_TOO_LONG_MESSAGE)
false
end
def rate_limit!
rate_limiter.tap do |rate_limiter|
if rate_limiter.limit_by_action(:organization_creation)
retry_after = RateLimitChecker::RETRY_AFTER[:organization_creation]
raise RateLimitChecker::LimitReached, retry_after
end
end
end
def rate_limiter
RateLimitChecker.new(current_user)
end
end

View file

@ -1,12 +1,22 @@
class RateLimitChecker
attr_reader :user, :action
# Values are seconds until a user can retry
RETRY_AFTER = {
article_update: 30,
image_upload: 30,
published_article_creation: 30
published_article_creation: 30,
organization_creation: 300
}.with_indifferent_access.freeze
CONFIG_LIMIT_KEYS = %i[
rate_limit_follow_count_daily
rate_limit_comment_creation
rate_limit_published_article_creation
rate_limit_image_upload
rate_limit_email_recipient
].freeze
def initialize(user = nil)
@user = user
end
@ -53,6 +63,11 @@ class RateLimitChecker
SiteConfig.rate_limit_email_recipient
end
def track_organization_creation
expires_in = RETRY_AFTER[:organization_creation].seconds
Rails.cache.increment("#{@user.id}_organization_creation", 1, expires_in: expires_in)
end
private
def check_comment_creation_limit
@ -65,6 +80,11 @@ class RateLimitChecker
SiteConfig.rate_limit_published_article_creation
end
def check_organization_creation_limit
Rails.cache.read("#{user.id}_organization_creation").to_i >=
SiteConfig.rate_limit_organization_creation
end
def check_image_upload_limit
Rails.cache.read("#{user.id}_image_upload").to_i >
SiteConfig.rate_limit_image_upload

View file

@ -69,6 +69,7 @@ class SiteConfig < RailsSettings::Base
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_organization_creation, type: :integer, default: 1
field :rate_limit_image_upload, type: :integer, default: 9
field :rate_limit_email_recipient, type: :integer, default: 5
field :rate_limit_article_update, type: :integer, default: 150

View file

@ -379,6 +379,16 @@
<div class="alert alert-info">Used to limit the amount of articles a user can create within 30 seconds</div>
</div>
<div class="form-group">
<%= f.label :rate_limit_organization_creation %>
<%= f.number_field :rate_limit_organization_creation,
class: "form-control",
value: SiteConfig.rate_limit_organization_creation,
min: 1,
placeholder: 1 %>
<div class="alert alert-info">Number of organizations a user can create within a 5 minute period</div>
</div>
<div class="form-group">
<%= f.label :rate_limit_image_upload %>
<%= f.number_field :rate_limit_image_upload,

View file

@ -55,10 +55,10 @@ error. Mappings for each index can be found in
In order to index the data from Postgres into Elasticsearch, we rely on
callbacks in our ActiveRecord models. Whenever a model is created or updated we
use an `after_commit` callback to enqueue a `Search::IndexToElasticsearchWorker`
which handles indexing the document into Elasticsearch. In order to translate
our ActiveRecord data to Elasticsearch we use serializers. Each model has its
own serializer which can be found in `app/serializers/search`
use an `after_commit` callback to enqueue a `Search::IndexWorker` which handles
indexing the document into Elasticsearch. In order to translate our ActiveRecord
data to Elasticsearch we use serializers. Each model has its own serializer
which can be found in `app/serializers/search`
### Searching Elasticsearch

View file

@ -98,6 +98,18 @@ RSpec.describe RateLimitChecker, type: :labor do
expect(rate_limit_checker.limit_by_action("article_update")).to be(false)
end
it "returns true if user has created too many organizations" do
allow(Rails.cache).
to receive(:read).with("#{user.id}_organization_creation").
and_return(SiteConfig.rate_limit_organization_creation + 1)
expect(rate_limit_checker.limit_by_action("organization_creation")).to be(true)
end
it "returns false if organization_creation limit has not been reached" do
expect(described_class.new(user).limit_by_action("organization_creation")).to be(false)
end
end
describe ".track_image_uploads" do

View file

@ -189,6 +189,12 @@ RSpec.describe "/internal/config", type: :request do
end.to change(SiteConfig, :rate_limit_published_article_creation).from(9).to(3)
end
it "updates rate_limit_organization_creation" do
expect do
post "/internal/config", params: { site_config: { rate_limit_organization_creation: 3 }, confirmation: confirmation_message }
end.to change(SiteConfig, :rate_limit_organization_creation).from(1).to(3)
end
it "updates rate_limit_image_upload" do
expect do
post "/internal/config", params: { site_config: { rate_limit_image_upload: 3 }, confirmation: confirmation_message }

View file

@ -28,14 +28,19 @@ RSpec.describe "UserOrganization", type: :request do
end
context "when creating a new org" do
let(:org_params) { build(:organization).attributes }
let(:rate_limiter) { RateLimitChecker.new(user) }
let(:create_org) { post "/organizations", params: { organization: org_params } }
before do
sign_in user
org_params = build(:organization).attributes
org_params["profile_image"] = Rack::Test::UploadedFile.new(Rails.root.join("app/assets/images/android-icon-36x36.png"), "image/jpeg")
post "/organizations", params: { organization: org_params }
allow(RateLimitChecker).to receive(:new).and_return(rate_limiter)
allow(rate_limiter).to receive(:limit_by_action).and_return(false)
end
it "creates the correct organization_membership association" do
create_org
org_membership = OrganizationMembership.first
expect(org_membership.persisted?).to eq true
expect(org_membership.user).to eq user
@ -44,9 +49,16 @@ RSpec.describe "UserOrganization", type: :request do
end
it "redirects to the proper org settings page" do
create_org
expect(response.status).to eq 302
expect(response.redirect_url).to include "/settings/organization/#{Organization.last.id}"
end
it "raises a Rate limit error if the rate limit is reached" do
allow(rate_limiter).to receive(:limit_by_action).and_return(true)
expect { create_org }.to raise_error(RateLimitChecker::LimitReached)
end
end
it "catches error if profile image file name is too long" do