diff --git a/app/controllers/internal/configs_controller.rb b/app/controllers/internal/configs_controller.rb
index 7a38f8325..e6e6687d3 100644
--- a/app/controllers/internal/configs_controller.rb
+++ b/app/controllers/internal/configs_controller.rb
@@ -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
diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb
index e4c0b523a..7d46680df 100644
--- a/app/controllers/organizations_controller.rb
+++ b/app/controllers/organizations_controller.rb
@@ -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
diff --git a/app/labor/rate_limit_checker.rb b/app/labor/rate_limit_checker.rb
index a2203bd04..aca7f5a6a 100644
--- a/app/labor/rate_limit_checker.rb
+++ b/app/labor/rate_limit_checker.rb
@@ -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
diff --git a/app/models/site_config.rb b/app/models/site_config.rb
index 5219aa343..4e3e25066 100644
--- a/app/models/site_config.rb
+++ b/app/models/site_config.rb
@@ -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
diff --git a/app/views/internal/configs/show.html.erb b/app/views/internal/configs/show.html.erb
index 1118e7632..bba93c4d1 100644
--- a/app/views/internal/configs/show.html.erb
+++ b/app/views/internal/configs/show.html.erb
@@ -379,6 +379,16 @@
Used to limit the amount of articles a user can create within 30 seconds
+
+
<%= f.label :rate_limit_image_upload %>
<%= f.number_field :rate_limit_image_upload,
diff --git a/docs/backend/elasticsearch.md b/docs/backend/elasticsearch.md
index 3896889e5..d1b88f401 100644
--- a/docs/backend/elasticsearch.md
+++ b/docs/backend/elasticsearch.md
@@ -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
diff --git a/spec/labor/rate_limit_checker_spec.rb b/spec/labor/rate_limit_checker_spec.rb
index 3c9a80397..d9ce43b6f 100644
--- a/spec/labor/rate_limit_checker_spec.rb
+++ b/spec/labor/rate_limit_checker_spec.rb
@@ -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
diff --git a/spec/requests/internal/configs_spec.rb b/spec/requests/internal/configs_spec.rb
index 4791f981f..c7ae86231 100644
--- a/spec/requests/internal/configs_spec.rb
+++ b/spec/requests/internal/configs_spec.rb
@@ -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 }
diff --git a/spec/requests/user/user_organization_spec.rb b/spec/requests/user/user_organization_spec.rb
index 3c0e754b7..86451e3ee 100644
--- a/spec/requests/user/user_organization_spec.rb
+++ b/spec/requests/user/user_organization_spec.rb
@@ -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