Create settings model for rate limits (#13381)
Co-authored-by: rhymes <rhymes@hey.com>
This commit is contained in:
parent
2f1dad4e37
commit
28dd50e718
26 changed files with 415 additions and 145 deletions
22
app/controllers/admin/settings/rate_limits_controller.rb
Normal file
22
app/controllers/admin/settings/rate_limits_controller.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
module Admin
|
||||
module Settings
|
||||
class RateLimitsController < Admin::ApplicationController
|
||||
def create
|
||||
result = ::RateLimits::SettingsUpsert.call(settings_params)
|
||||
|
||||
if result.success?
|
||||
Audit::Logger.log(:internal, current_user, params.dup)
|
||||
redirect_to admin_config_path, notice: "Site configuration was successfully updated."
|
||||
else
|
||||
redirect_to admin_config_path, alert: "😭 #{result.errors.to_sentence}"
|
||||
end
|
||||
end
|
||||
|
||||
def settings_params
|
||||
params
|
||||
.require(:settings_rate_limit)
|
||||
.permit(*::Settings::RateLimit.keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -108,7 +108,7 @@ class UserDecorator < ApplicationDecorator
|
|||
end
|
||||
|
||||
def considered_new?
|
||||
min_days = SiteConfig.user_considered_new_days
|
||||
min_days = Settings::RateLimit.user_considered_new_days
|
||||
return false unless min_days.positive?
|
||||
|
||||
created_at.after?(min_days.days.ago)
|
||||
|
|
|
|||
|
|
@ -8,91 +8,91 @@ module RateLimitCheckerHelper
|
|||
end
|
||||
|
||||
CONFIGURABLE_RATES = {
|
||||
rate_limit_published_article_creation: {
|
||||
published_article_creation: {
|
||||
min: 0,
|
||||
placeholder: 9,
|
||||
title: "Limit number of posts created",
|
||||
description: "How many posts can someone create within any 30 second period?"
|
||||
},
|
||||
rate_limit_published_article_antispam_creation: {
|
||||
published_article_antispam_creation: {
|
||||
min: 0,
|
||||
placeholder: 1,
|
||||
title: "Limit number of posts created by a new member",
|
||||
description: new_user_message("posts")
|
||||
},
|
||||
rate_limit_article_update: {
|
||||
article_update: {
|
||||
min: 1,
|
||||
placeholder: 30,
|
||||
title: "Limit number of updates to a post",
|
||||
description: "How many updates can someone make within any 30 second period? Update API docs when changed."
|
||||
},
|
||||
rate_limit_image_upload: {
|
||||
image_upload: {
|
||||
min: 0,
|
||||
placeholder: 9,
|
||||
title: "Limit number of images uploaded",
|
||||
description: "How many images can someone upload within any 30 second period?"
|
||||
},
|
||||
rate_limit_user_update: {
|
||||
user_update: {
|
||||
min: 1,
|
||||
placeholder: 5,
|
||||
title: "Limit number of changes someone can make to their account",
|
||||
description: "How many changes can someone make to their user account within any 30 second period?"
|
||||
},
|
||||
rate_limit_follow_count_daily: {
|
||||
follow_count_daily: {
|
||||
min: 0,
|
||||
placeholder: 500,
|
||||
title: "Limit number of followers someone can follow daily",
|
||||
description: "How many people can someone follow in a day?"
|
||||
},
|
||||
rate_limit_reaction_creation: {
|
||||
reaction_creation: {
|
||||
min: 1,
|
||||
placeholder: 10,
|
||||
title: "Limit number of reactions to a post or comment",
|
||||
description: "How many times can someone react to a post or comment within any 30 second period?"
|
||||
},
|
||||
rate_limit_feedback_message_creation: {
|
||||
feedback_message_creation: {
|
||||
min: 1,
|
||||
placeholder: 5,
|
||||
title: "Limit number of times someone can report abuse",
|
||||
description: "How many times can someone report abuse within any 5 minute period?"
|
||||
},
|
||||
rate_limit_comment_creation: {
|
||||
comment_creation: {
|
||||
min: 0,
|
||||
placeholder: 9,
|
||||
title: "Limit number of comments created",
|
||||
description: "How many comments can someone create within any 30 second period?"
|
||||
},
|
||||
rate_limit_comment_antispam_creation: {
|
||||
comment_antispam_creation: {
|
||||
min: 0,
|
||||
placeholder: 1,
|
||||
title: "Limit number of comments created by a new member",
|
||||
description: new_user_message("comments")
|
||||
},
|
||||
rate_limit_listing_creation: {
|
||||
listing_creation: {
|
||||
min: 1,
|
||||
placeholder: 1,
|
||||
title: "Limit number of listings created",
|
||||
description: "How many listings can someone create within any 1 minute period?"
|
||||
},
|
||||
rate_limit_organization_creation: {
|
||||
organization_creation: {
|
||||
min: 1,
|
||||
placeholder: 1,
|
||||
title: "Limit number of organizations created",
|
||||
description: "How many organizations can someone create within any 5 minute period?"
|
||||
},
|
||||
rate_limit_user_subscription_creation: {
|
||||
user_subscription_creation: {
|
||||
min: 0,
|
||||
placeholder: 3,
|
||||
title: "Limit number of times someone can subscribe to mailing list liquid tag",
|
||||
description: "How many times can someone subscribe to a mailing list within any 30 second period?"
|
||||
},
|
||||
rate_limit_email_recipient: {
|
||||
email_recipient: {
|
||||
min: 0,
|
||||
placeholder: 5,
|
||||
title: "Limit number of general emails we send",
|
||||
description: "How many emails can we send to someone within any 2 minute period?"
|
||||
},
|
||||
rate_limit_send_email_confirmation: {
|
||||
send_email_confirmation: {
|
||||
min: 1,
|
||||
placeholder: 2,
|
||||
title: "Limit number of confirmation emails we send",
|
||||
|
|
|
|||
17
app/lib/constants/settings/rate_limit.rb
Normal file
17
app/lib/constants/settings/rate_limit.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
module Constants
|
||||
module Settings
|
||||
module RateLimit
|
||||
DETAILS = {
|
||||
spam_trigger_terms: {
|
||||
description: "Individual (case insensitive) phrases that trigger spam alerts, comma separated.",
|
||||
placeholder: "used cars near you, pokemon go hack"
|
||||
},
|
||||
user_considered_new_days: {
|
||||
description: "The number of days a user is considered new. The default is 3 days, but you "\
|
||||
"can disable this entirely by inputting 0.",
|
||||
placeholder: ::SiteConfig.user_considered_new_days
|
||||
}
|
||||
}.freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -172,10 +172,6 @@ module Constants
|
|||
description: "A place for an alternate logo, if you have one. Used throughout member onboarding and in some sign in forms.",
|
||||
placeholder: IMAGE_PLACEHOLDER
|
||||
},
|
||||
spam_trigger_terms: {
|
||||
description: "Individual (case insensitive) phrases that trigger spam alerts, comma separated.",
|
||||
placeholder: "used cars near you, pokemon go hack"
|
||||
},
|
||||
shop_url: {
|
||||
description: "Used as the shop url of the community",
|
||||
placeholder: "https://shop.url"
|
||||
|
|
@ -229,10 +225,6 @@ module Constants
|
|||
description: "Used as the twitter hashtag of the community",
|
||||
placeholder: "#DEVCommunity"
|
||||
},
|
||||
user_considered_new_days: {
|
||||
description: "The number of days a user is considered new. The default is 3 days, but you can disable this entirely by inputting 0.",
|
||||
placeholder: ::SiteConfig.user_considered_new_days
|
||||
},
|
||||
video_encoder_key: {
|
||||
description: "Secret key used to allow AWS video encoding through the VideoStatesController",
|
||||
placeholder: ""
|
||||
|
|
|
|||
31
app/models/settings/rate_limit.rb
Normal file
31
app/models/settings/rate_limit.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
module Settings
|
||||
class RateLimit < RailsSettings::Base
|
||||
self.table_name = :settings_rate_limits
|
||||
|
||||
# The configuration is cached, change this if you want to force update
|
||||
# the cache, or call Settings::RateLimit.clear_cache
|
||||
cache_prefix { "v1" }
|
||||
|
||||
field :article_update, type: :integer, default: 30
|
||||
field :comment_antispam_creation, type: :integer, default: 1
|
||||
field :comment_creation, type: :integer, default: 9
|
||||
field :email_recipient, type: :integer, default: 5
|
||||
field :feedback_message_creation, type: :integer, default: 5
|
||||
field :follow_count_daily, type: :integer, default: 500
|
||||
field :image_upload, type: :integer, default: 9
|
||||
field :listing_creation, type: :integer, default: 1
|
||||
field :organization_creation, type: :integer, default: 1
|
||||
field :published_article_antispam_creation, type: :integer, default: 1
|
||||
field :published_article_creation, type: :integer, default: 9
|
||||
field :reaction_creation, type: :integer, default: 10
|
||||
field :send_email_confirmation, type: :integer, default: 2
|
||||
field :spam_trigger_terms, type: :array, default: []
|
||||
field :user_considered_new_days, type: :integer, default: 3
|
||||
field :user_subscription_creation, type: :integer, default: 3
|
||||
field :user_update, type: :integer, default: 15
|
||||
|
||||
def self.get_default(field)
|
||||
get_field(field)[:default]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -142,6 +142,8 @@ class SiteConfig < RailsSettings::Base
|
|||
field :prefer_manual_suggested_users, type: :boolean, default: false
|
||||
|
||||
# Rate limits and spam prevention
|
||||
# NOTE: @citizen428 These will be removed once we migrated to the new settings
|
||||
# model across the fleet.
|
||||
field :rate_limit_follow_count_daily, type: :integer, default: 500
|
||||
field :rate_limit_comment_creation, type: :integer, default: 9
|
||||
field :rate_limit_comment_antispam_creation, type: :integer, default: 1
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class RateLimitChecker
|
|||
def limit_by_email_recipient_address(address)
|
||||
# This is related to the recipient, not the "user" initiator, like in action.
|
||||
EmailMessage.where(to: address).where("sent_at > ?", 2.minutes.ago).size >
|
||||
SiteConfig.rate_limit_email_recipient
|
||||
Settings::RateLimit.email_recipient
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -78,39 +78,39 @@ class RateLimitChecker
|
|||
end
|
||||
|
||||
def action_rate_limit(action)
|
||||
SiteConfig.public_send("rate_limit_#{action}")
|
||||
Settings::RateLimit.public_send(action)
|
||||
end
|
||||
|
||||
def check_comment_creation_limit
|
||||
user.comments.where("created_at > ?", 30.seconds.ago).size >
|
||||
SiteConfig.rate_limit_comment_creation
|
||||
Settings::RateLimit.comment_creation
|
||||
end
|
||||
|
||||
def check_published_article_creation_limit
|
||||
# TODO: Vaidehi Joshi - We should make this time frame configurable.
|
||||
user.articles.published.where("created_at > ?", 30.seconds.ago).size >
|
||||
SiteConfig.rate_limit_published_article_creation
|
||||
Settings::RateLimit.published_article_creation
|
||||
end
|
||||
|
||||
def check_published_article_antispam_creation_limit
|
||||
# TODO: Vaidehi Joshi - We should make this time frame configurable.
|
||||
user.articles.published.where("created_at > ?", 5.minutes.ago).size >
|
||||
SiteConfig.rate_limit_published_article_antispam_creation
|
||||
Settings::RateLimit.published_article_antispam_creation
|
||||
end
|
||||
|
||||
def check_comment_antispam_creation_limit
|
||||
# TODO: We should make this time frame configurable.
|
||||
user.comments.where(created_at: 5.minutes.ago...).size >
|
||||
SiteConfig.rate_limit_comment_antispam_creation
|
||||
Settings::RateLimit.comment_antispam_creation
|
||||
end
|
||||
|
||||
def check_follow_account_limit
|
||||
user_today_follow_count > SiteConfig.rate_limit_follow_count_daily
|
||||
user_today_follow_count > Settings::RateLimit.follow_count_daily
|
||||
end
|
||||
|
||||
def user_today_follow_count
|
||||
following_users_count = user.following_users_count
|
||||
return following_users_count if following_users_count < SiteConfig.rate_limit_follow_count_daily
|
||||
return following_users_count if following_users_count < Settings::RateLimit.follow_count_daily
|
||||
|
||||
now = Time.zone.now
|
||||
user.follows.where(created_at: (now.beginning_of_day..now)).size
|
||||
|
|
|
|||
41
app/services/rate_limits/settings_upsert.rb
Normal file
41
app/services/rate_limits/settings_upsert.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
module RateLimits
|
||||
class SettingsUpsert
|
||||
attr_reader :errors
|
||||
|
||||
def self.call(configs)
|
||||
new(configs).call
|
||||
end
|
||||
|
||||
def initialize(configs)
|
||||
@configs = configs
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def call
|
||||
upsert_configs
|
||||
self
|
||||
end
|
||||
|
||||
def success?
|
||||
@errors.none?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# NOTE: @citizen428 - This was adapted from Settings::Upsert. I'll see if
|
||||
# a pattern for refactoring emerges in the future, but for now I'll leave
|
||||
# this as-is.
|
||||
def upsert_configs
|
||||
@configs.each do |key, value|
|
||||
if value.is_a?(Array) && value.any?
|
||||
Settings::RateLimit.public_send("#{key}=", value.reject(&:blank?))
|
||||
elsif value.present?
|
||||
Settings::RateLimit.public_send("#{key}=", value.strip)
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@errors << e.message
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1071,7 +1071,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_for(SiteConfig.new, url: admin_config_path) do |f| %>
|
||||
<%= form_for(Settings::RateLimit.new, url: admin_settings_rate_limits_path) do |f| %>
|
||||
<div class="card mt-3">
|
||||
<%= render partial: "admin/shared/card_header",
|
||||
locals: {
|
||||
|
|
@ -1083,35 +1083,35 @@
|
|||
<div id="rateLimitsBodyContainer" class="card-body collapse hide" aria-labelledby="rateLimitsBodyContainer">
|
||||
<fieldset class="grid gap-4">
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :user_considered_new_days %>
|
||||
<%= admin_config_description Constants::SiteConfig::DETAILS[:user_considered_new_days][:description] %>
|
||||
<%= admin_config_label :user_considered_new_days, model: Settings::RateLimit %>
|
||||
<%= admin_config_description Constants::Settings::RateLimit::DETAILS[:user_considered_new_days][:description] %>
|
||||
<%= f.number_field :user_considered_new_days,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.user_considered_new_days,
|
||||
placeholder: Constants::SiteConfig::DETAILS[:user_considered_new_days][:placeholder] %>
|
||||
value: Settings::RateLimit.user_considered_new_days,
|
||||
placeholder: Constants::Settings::RateLimit::DETAILS[:user_considered_new_days][:placeholder] %>
|
||||
</div>
|
||||
|
||||
<% configurable_rate_limits.each do |key, field_hash| %>
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label field_hash[:title] %>
|
||||
<%= admin_config_label field_hash[:title], model: Settings::RateLimit %>
|
||||
<p class="crayons-field__description">
|
||||
<%= field_hash[:description] %>
|
||||
</p>
|
||||
<%= f.number_field key,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.public_send(key),
|
||||
value: Settings::RateLimit.public_send(key),
|
||||
min: field_hash[:min],
|
||||
placeholder: field_hash[:placeholder] %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :spam_trigger_terms %>
|
||||
<%= admin_config_description Constants::SiteConfig::DETAILS[:spam_trigger_terms][:description] %>
|
||||
<%= admin_config_label :spam_trigger_terms, model: Settings::RateLimit %>
|
||||
<%= admin_config_description Constants::Settings::RateLimit::DETAILS[:spam_trigger_terms][:description] %>
|
||||
<%= f.text_area :spam_trigger_terms,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.spam_trigger_terms.join(","),
|
||||
placeholder: Constants::SiteConfig::DETAILS[:spam_trigger_terms][:placeholder] %>
|
||||
value: Settings::RateLimit.spam_trigger_terms.join(","),
|
||||
placeholder: Constants::Settings::RateLimit::DETAILS[:spam_trigger_terms][:placeholder] %>
|
||||
</div>
|
||||
<%= render "form_submission", f: f %>
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<% if @followed_tags.any? %>
|
||||
<%= javascript_packs_with_chunks_tag "dashboardTagsDisableUnchangedButtons", defer: true %>
|
||||
|
||||
<div class="crayons-notice p-4 px-6 mb-4 mx-2 m:mx-0" aria-live="polite">
|
||||
<div class="crayons-notice p-4 px-6 mb-4 mx-2 m:mx-0" aria-live="polite">
|
||||
Adjust tag weight to modify your home feed. Higher values mean more appearances for that tag.
|
||||
<span class="crayons-indicator crayons-indicator--outlined crayons-indicator--accent ml-3">Default 1.0</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div id="sign-in-password-form" class="mt-8 mb-6 crayons-card p-7 align-left mx-auto" style="max-width:580px;">
|
||||
<% if flash[:notice] %>
|
||||
<div class="crayons-notice crayons-notice--danger mb-6" role="alert" >
|
||||
<div class="crayons-notice crayons-notice--danger mb-6" role="alert">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ Rails.application.routes.draw do
|
|||
namespace :settings do
|
||||
resources :authentications, only: [:create]
|
||||
resources :campaigns, only: [:create]
|
||||
resources :rate_limits, only: [:create]
|
||||
end
|
||||
namespace :users do
|
||||
resources :gdpr_delete_requests, only: %i[index destroy]
|
||||
|
|
|
|||
49
cypress/integration/adminFlows/config/rateLimitSection.js
Normal file
49
cypress/integration/adminFlows/config/rateLimitSection.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
describe('Campaign Section', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginUser(user);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rate limit settings', () => {
|
||||
it('can change for how many days a user is considered new', () => {
|
||||
cy.get('@user').then(({ username }) => {
|
||||
cy.visit('/admin/config');
|
||||
cy.get('#new_settings_rate_limit').as('rateLimitSectionForm');
|
||||
|
||||
cy.get('@rateLimitSectionForm')
|
||||
.findByText('Rate limits and anti-spam')
|
||||
.click();
|
||||
|
||||
cy.get('@rateLimitSectionForm')
|
||||
.get('#settings_rate_limit_user_considered_new_days')
|
||||
.clear()
|
||||
.type('42');
|
||||
|
||||
cy.get('@rateLimitSectionForm')
|
||||
.findByPlaceholderText('Confirmation text')
|
||||
.type(
|
||||
`My username is @${username} and this action is 100% safe and appropriate.`,
|
||||
);
|
||||
|
||||
cy.get('@rateLimitSectionForm')
|
||||
.findByText('Update Site Configuration')
|
||||
.click();
|
||||
|
||||
cy.url().should('contains', '/admin/config');
|
||||
|
||||
cy.findByText('Site configuration was successfully updated.').should(
|
||||
'be.visible',
|
||||
);
|
||||
|
||||
cy.get('#settings_rate_limit_user_considered_new_days').should(
|
||||
'have.value',
|
||||
'42',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -24,8 +24,12 @@ describe('Show log in modal', () => {
|
|||
cy.findAllByText('Test article').last().click();
|
||||
|
||||
cy.findByRole('button', { name: 'Like' }).as('heartReaction');
|
||||
cy.findByRole('button', { name: 'React with unicorn' }).as('unicornReaction');
|
||||
cy.findByRole('button', { name: 'Add to reading list' }).as('bookmarkReaction');
|
||||
cy.findByRole('button', { name: 'React with unicorn' }).as(
|
||||
'unicornReaction',
|
||||
);
|
||||
cy.findByRole('button', { name: 'Add to reading list' }).as(
|
||||
'bookmarkReaction',
|
||||
);
|
||||
|
||||
['@heartReaction', '@unicornReaction', '@bookmarkReaction'].forEach(
|
||||
(reaction) => {
|
||||
|
|
@ -46,7 +50,9 @@ describe('Show log in modal', () => {
|
|||
|
||||
it('should show login modal for comment subscription', () => {
|
||||
cy.findAllByText('Test article').last().click();
|
||||
cy.findByRole('button', { name: /Subscribe/ }).as('subscribe').click();
|
||||
cy.findByRole('button', { name: /Subscribe/ })
|
||||
.as('subscribe')
|
||||
.click();
|
||||
|
||||
cy.findByTestId('modal-container').as('modal');
|
||||
cy.get('@modal').findByText('Log in to continue').should('exist');
|
||||
|
|
|
|||
16
db/migrate/20210414033457_create_settings_rate_limits.rb
Normal file
16
db/migrate/20210414033457_create_settings_rate_limits.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class CreateSettingsRateLimits < ActiveRecord::Migration[6.1]
|
||||
def self.up
|
||||
create_table :settings_rate_limits do |t|
|
||||
t.string :var, null: false
|
||||
t.text :value, null: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :settings_rate_limits, :var, unique: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :settings_rate_limits
|
||||
end
|
||||
end
|
||||
10
db/schema.rb
10
db/schema.rb
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_04_07_172628) do
|
||||
ActiveRecord::Schema.define(version: 2021_04_14_033457) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "citext"
|
||||
|
|
@ -1066,6 +1066,14 @@ ActiveRecord::Schema.define(version: 2021_04_07_172628) do
|
|||
t.index ["var"], name: "index_settings_campaigns_on_var", unique: true
|
||||
end
|
||||
|
||||
create_table "settings_rate_limits", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.text "value"
|
||||
t.string "var", null: false
|
||||
t.index ["var"], name: "index_settings_rate_limits_on_var", unique: true
|
||||
end
|
||||
|
||||
create_table "site_configs", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ Settings managed via your ENV can be found in
|
|||
We use this gem for managing settings used within the app's business logic.
|
||||
Examples:
|
||||
|
||||
- `main_social_image`
|
||||
- `rate_limit_follow_count_daily`
|
||||
- `suggested_tags`
|
||||
- `SiteConfig.main_social_image`
|
||||
- `Settings::RateLimit.follow_count_daily`
|
||||
- `Settings::Authentication.twitter_secret`
|
||||
|
||||
These settings can be accessed via the
|
||||
[`SiteConfig`](https://github.com/forem/forem/blob/main/app/models/site_config.rb)
|
||||
object and viewed / modified via `/admin/config` (see
|
||||
[the Admin guide](/admin)).
|
||||
[`SiteConfig`](https://github.com/forem/forem/blob/master/app/models/site_config.rb)
|
||||
object and various models in the `Settings::` namespace and viewed / modified
|
||||
via `/admin/config` (see [the Admin guide](/admin)).
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
module DataUpdateScripts
|
||||
class UpdateUserUpdateRateLimitDefault
|
||||
def run
|
||||
return if SiteConfig.rate_limit_user_update > 5
|
||||
return if Settings::RateLimit.user_update > 5
|
||||
|
||||
SiteConfig.rate_limit_user_update = 15
|
||||
Settings::RateLimit.user_update = 15
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
module DataUpdateScripts
|
||||
class MoveRateLimitSettings
|
||||
RENAMED_RATE_LIMIT_SETTINGS = %w[
|
||||
article_update
|
||||
comment_antispam_creation
|
||||
comment_creation
|
||||
email_recipient
|
||||
feedback_message_creation
|
||||
follow_count_daily
|
||||
image_upload
|
||||
listing_creation
|
||||
organization_creation
|
||||
published_article_antispam_creation
|
||||
published_article_creation
|
||||
reaction_creation
|
||||
send_email_confirmation
|
||||
user_subscription_creation
|
||||
user_update
|
||||
].freeze
|
||||
|
||||
def run
|
||||
return if Settings::RateLimit.any?
|
||||
|
||||
RENAMED_RATE_LIMIT_SETTINGS.each do |setting|
|
||||
Settings::RateLimit.public_send(
|
||||
"#{setting}=",
|
||||
SiteConfig.public_send("rate_limit_#{setting}"),
|
||||
)
|
||||
end
|
||||
|
||||
Settings::RateLimit.spam_trigger_terms = SiteConfig.spam_trigger_terms
|
||||
Settings::RateLimit.user_considered_new_days =
|
||||
SiteConfig.user_considered_new_days
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,8 +3,9 @@ require "rails_helper"
|
|||
describe RateLimitCheckerHelper, type: :helper do
|
||||
describe "#configurable_rate_limits" do
|
||||
it "returns a hash with the right structure" do
|
||||
settings_keys = Settings::RateLimit.keys.map(&:to_sym)
|
||||
helper.configurable_rate_limits.each do |key, value_hash|
|
||||
expect(key).to match(/\Arate_limit/)
|
||||
expect(settings_keys).to include(key)
|
||||
expect(value_hash.keys).to match_array(%i[title min placeholder description])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
require "rails_helper"
|
||||
require Rails.root.join(
|
||||
"lib/data_update_scripts/20210414060839_move_rate_limit_settings.rb",
|
||||
)
|
||||
|
||||
describe DataUpdateScripts::MoveRateLimitSettings do
|
||||
it "migrates settings from SiteConfig to Settings::RateLimit" do
|
||||
allow(SiteConfig).to receive(:rate_limit_follow_count_daily).and_return(23)
|
||||
allow(SiteConfig).to receive(:user_considered_new_days).and_return(42)
|
||||
|
||||
expect do
|
||||
described_class.new.run
|
||||
end
|
||||
.to change(Settings::RateLimit, :follow_count_daily)
|
||||
.and change(Settings::RateLimit, :user_considered_new_days)
|
||||
end
|
||||
end
|
||||
|
|
@ -6,12 +6,12 @@ require Rails.root.join(
|
|||
describe DataUpdateScripts::UpdateUserUpdateRateLimitDefault do
|
||||
it "updates rate limit if 5 or less" do
|
||||
described_class.new.run
|
||||
expect(SiteConfig.rate_limit_user_update).to eq(15)
|
||||
expect(Settings::RateLimit.user_update).to eq(15)
|
||||
end
|
||||
|
||||
it "does NOT update the rate limit if greater than 5" do
|
||||
allow(SiteConfig).to receive(:rate_limit_user_update).and_return(10)
|
||||
allow(Settings::RateLimit).to receive(:user_update).and_return(10)
|
||||
described_class.new.run
|
||||
expect(SiteConfig.rate_limit_user_update).to eq(10)
|
||||
expect(Settings::RateLimit.user_update).to eq(10)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -634,122 +634,152 @@ RSpec.describe "/admin/config", type: :request do
|
|||
end
|
||||
|
||||
describe "Rate Limits and spam" do
|
||||
it "updates rate_limit_follow_count_daily" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_follow_count_daily)
|
||||
it "updates follow_count_daily" do
|
||||
default_value = Settings::RateLimit.get_default(:follow_count_daily)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_follow_count_daily: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_follow_count_daily).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { follow_count_daily: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :follow_count_daily).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_comment_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_comment_creation)
|
||||
it "updates comment_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:comment_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_comment_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_comment_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { comment_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :comment_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_published_article_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_published_article_creation)
|
||||
it "updates published_article_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:published_article_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_published_article_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_published_article_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { published_article_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :published_article_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_published_article_antispam_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_published_article_antispam_creation)
|
||||
it "updates published_article_antispam_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:published_article_antispam_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_published_article_antispam_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_published_article_antispam_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { published_article_antispam_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :published_article_antispam_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_organization_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_organization_creation)
|
||||
it "updates organization_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:organization_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_organization_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_organization_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { organization_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :organization_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_image_upload" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_image_upload)
|
||||
it "updates image_upload" do
|
||||
default_value = Settings::RateLimit.get_default(:image_upload)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_image_upload: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_image_upload).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { image_upload: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :image_upload).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_email_recipient" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_email_recipient)
|
||||
it "updates email_recipient" do
|
||||
default_value = Settings::RateLimit.get_default(:email_recipient)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_email_recipient: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_email_recipient).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { email_recipient: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :email_recipient).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_user_subscription_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_user_subscription_creation)
|
||||
it "updates user_subscription_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:user_subscription_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_user_subscription_creation: 1 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_user_subscription_creation).from(default_value).to(1)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { user_subscription_creation: 1 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :user_subscription_creation).from(default_value).to(1)
|
||||
end
|
||||
|
||||
it "updates rate_limit_article_update" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_article_update)
|
||||
it "updates article_update" do
|
||||
default_value = Settings::RateLimit.get_default(:article_update)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_article_update: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_article_update).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { article_update: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :article_update).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_user_update" do
|
||||
it "updates user_update" do
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_user_update: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_user_update).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { user_update: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :user_update).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_feedback_message_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_feedback_message_creation)
|
||||
it "updates feedback_message_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:feedback_message_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_feedback_message_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_feedback_message_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { feedback_message_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :feedback_message_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_listing_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_listing_creation)
|
||||
it "updates listing_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:listing_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_listing_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_listing_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { listing_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :listing_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_reaction_creation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_reaction_creation)
|
||||
it "updates reaction_creation" do
|
||||
default_value = Settings::RateLimit.get_default(:reaction_creation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_reaction_creation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_reaction_creation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { reaction_creation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :reaction_creation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates rate_limit_send_email_confirmation" do
|
||||
default_value = SiteConfig.get_default(:rate_limit_send_email_confirmation)
|
||||
it "updates send_email_confirmation" do
|
||||
default_value = Settings::RateLimit.get_default(:send_email_confirmation)
|
||||
expect do
|
||||
post "/admin/config", params: { site_config: { rate_limit_send_email_confirmation: 3 },
|
||||
confirmation: confirmation_message }
|
||||
end.to change(SiteConfig, :rate_limit_send_email_confirmation).from(default_value).to(3)
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { send_email_confirmation: 3 },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
end.to change(Settings::RateLimit, :send_email_confirmation).from(default_value).to(3)
|
||||
end
|
||||
|
||||
it "updates spam_trigger_terms" do
|
||||
spam_trigger_terms = "hey, pokemon go hack"
|
||||
post "/admin/config", params: { site_config: { spam_trigger_terms: spam_trigger_terms },
|
||||
confirmation: confirmation_message }
|
||||
expect(SiteConfig.spam_trigger_terms).to eq(["hey", "pokemon go hack"])
|
||||
post admin_settings_rate_limits_path, params: {
|
||||
settings_rate_limit: { spam_trigger_terms: spam_trigger_terms },
|
||||
confirmation: confirmation_message
|
||||
}
|
||||
expect(Settings::RateLimit.spam_trigger_terms).to eq(["hey", "pokemon go hack"])
|
||||
end
|
||||
|
||||
it "updates recaptcha_site_key and recaptcha_secret_key" do
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ RSpec.describe "Follows #create", type: :request do
|
|||
|
||||
allow(rate_limit_checker)
|
||||
.to receive(:user_today_follow_count)
|
||||
.and_return(SiteConfig.rate_limit_follow_count_daily + 1)
|
||||
.and_return(Settings::RateLimit.follow_count_daily + 1)
|
||||
|
||||
allow(RateLimitChecker)
|
||||
.to receive(:new)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "raises an error if no unique component is present for a cache key" do
|
||||
action = described_class::ACTION_LIMITERS.keys.first
|
||||
limiter = described_class.new(build(:user))
|
||||
expect { limiter.limit_by_action(action) }.to raise_error("Invalid Cache Key: no unique component present")
|
||||
expect { limiter.limit_by_action(action) }
|
||||
.to raise_error("Invalid Cache Key: no unique component present")
|
||||
end
|
||||
|
||||
# We check the excepted limits against the database, rather than our cache.
|
||||
|
|
@ -34,7 +35,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "returns true if #{action} limit has been reached" do
|
||||
allow(Rails.cache).to receive(:read).with(
|
||||
cache_key(action), raw: true
|
||||
).and_return(SiteConfig.public_send("rate_limit_#{action}") + 1)
|
||||
).and_return(Settings::RateLimit.public_send(action) + 1)
|
||||
|
||||
expect(rate_limit_checker.limit_by_action(action)).to be(true)
|
||||
end
|
||||
|
|
@ -42,7 +43,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "returns false if #{action} limit has NOT been reached" do
|
||||
allow(Rails.cache).to receive(:read).with(
|
||||
cache_key(action), raw: true
|
||||
).and_return(SiteConfig.public_send("rate_limit_#{action}"))
|
||||
).and_return(Settings::RateLimit.public_send(action))
|
||||
|
||||
expect(rate_limit_checker.limit_by_action(action)).to be(false)
|
||||
end
|
||||
|
|
@ -50,7 +51,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
|
||||
context "when creating comments" do
|
||||
before do
|
||||
allow(SiteConfig).to receive(:rate_limit_comment_creation).and_return(1)
|
||||
allow(Settings::RateLimit).to receive(:comment_creation).and_return(1)
|
||||
end
|
||||
|
||||
it "returns true if too many comments at once" do
|
||||
|
|
@ -64,13 +65,13 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
end
|
||||
|
||||
it "returns true if too many published articles at once and potentially spammy" do
|
||||
allow(SiteConfig).to receive(:rate_limit_published_article_antispam_creation).and_return(1)
|
||||
allow(Settings::RateLimit).to receive(:published_article_antispam_creation).and_return(1)
|
||||
create_list(:article, 2, user_id: user.id, published: true)
|
||||
expect(rate_limit_checker.limit_by_action("published_article_antispam_creation")).to be(true)
|
||||
end
|
||||
|
||||
it "returns true if too many published articles at once" do
|
||||
allow(SiteConfig).to receive(:rate_limit_published_article_creation).and_return(1)
|
||||
allow(Settings::RateLimit).to receive(:published_article_creation).and_return(1)
|
||||
create_list(:article, 2, user_id: user.id, published: true)
|
||||
expect(rate_limit_checker.limit_by_action("published_article_creation")).to be(true)
|
||||
end
|
||||
|
|
@ -78,7 +79,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "returns true if a user has followed more than <daily_limit> accounts today" do
|
||||
allow(rate_limit_checker)
|
||||
.to receive(:user_today_follow_count)
|
||||
.and_return(SiteConfig.rate_limit_follow_count_daily + 1)
|
||||
.and_return(Settings::RateLimit.follow_count_daily + 1)
|
||||
|
||||
expect(rate_limit_checker.limit_by_action("follow_account")).to be(true)
|
||||
end
|
||||
|
|
@ -86,7 +87,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "returns false if a user's following_users_count is less than <daily_limit>" do
|
||||
allow(user)
|
||||
.to receive(:following_users_count)
|
||||
.and_return(SiteConfig.rate_limit_follow_count_daily - 1)
|
||||
.and_return(Settings::RateLimit.follow_count_daily - 1)
|
||||
|
||||
expect(rate_limit_checker.limit_by_action("follow_account")).to be(false)
|
||||
end
|
||||
|
|
@ -94,7 +95,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "returns false if a user has followed less than <daily_limit> accounts today" do
|
||||
allow(rate_limit_checker)
|
||||
.to receive(:user_today_follow_count)
|
||||
.and_return(SiteConfig.rate_limit_follow_count_daily)
|
||||
.and_return(Settings::RateLimit.follow_count_daily)
|
||||
|
||||
expect(rate_limit_checker.limit_by_action("follow_account")).to be(false)
|
||||
end
|
||||
|
|
@ -110,7 +111,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
it "logs a rate limit hit to datadog" do
|
||||
allow(Rails.cache)
|
||||
.to receive(:read).with("#{user.id}_organization_creation", raw: true)
|
||||
.and_return(SiteConfig.rate_limit_organization_creation + 1)
|
||||
.and_return(Settings::RateLimit.organization_creation + 1)
|
||||
allow(ForemStatsClient).to receive(:increment)
|
||||
described_class.new(user).limit_by_action("organization_creation")
|
||||
|
||||
|
|
@ -147,7 +148,7 @@ RSpec.describe RateLimitChecker, type: :service do
|
|||
|
||||
describe "#limit_by_email_recipient_address" do
|
||||
before do
|
||||
allow(SiteConfig).to receive(:rate_limit_email_recipient).and_return(1)
|
||||
allow(Settings::RateLimit).to receive(:email_recipient).and_return(1)
|
||||
end
|
||||
|
||||
it "returns true if too many emails are sent to the same recipient" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue