[deploy] Rate Limit Organization Creation (#7595)
This commit is contained in:
parent
1d6c1fe445
commit
74e75b4067
9 changed files with 85 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue