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_organization_creation %> + <%= f.number_field :rate_limit_organization_creation, + class: "form-control", + value: SiteConfig.rate_limit_organization_creation, + min: 1, + placeholder: 1 %> +
Number of organizations a user can create within a 5 minute period
+
+
<%= 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