-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Remove allow2ban banning / filtering for registration, just throttle instead. * Add new settings section for rate limiting * Add rake tasks to enable/disable/reset settings when necessary * Standardize rate limiting with categories that can be optionally disabled while keeping the rest of the rate limiting active.
- Loading branch information
1 parent
c3b38da
commit 516e183
Showing
18 changed files
with
598 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# frozen_string_literal: true | ||
|
||
module SettingSections | ||
# Settings related to the rate-limiting subsystem. | ||
# | ||
# @note The `enabled` option takes precedence over the more granular options | ||
# when set to `false`. | ||
class RateLimiting < Base | ||
STATUS_TABLE_OPTIONS = { | ||
title: "Manifold Rate-limiting Settings", | ||
headings: [ | ||
"Category", | ||
{ value: "On", alignment: :right }, | ||
{ value: "On (Effective)", alignment: :right } | ||
], | ||
style: { | ||
width: 78, | ||
}, | ||
}.freeze | ||
|
||
attribute :enabled, :boolean, default: true | ||
|
||
ManifoldEnv::Types::THROTTLED_CATEGORIES.each do |category| | ||
attribute category, :boolean, default: true | ||
end | ||
|
||
# @param [ManifoldEnv::Types::ThrottledCategory] name | ||
def category?(name) | ||
name.in?(ManifoldEnv::Types::THROTTLED_CATEGORIES) | ||
end | ||
|
||
# @param [ManifoldEnv::Types::ThrottledCategory] name | ||
def category_disabled?(name) | ||
category?(name) && !__send__(name) | ||
end | ||
|
||
# @see #toggle! | ||
# @param [ManifoldEnv::Types::ThrottledCategory] category | ||
# @return [void] | ||
def disable!(category) | ||
toggle! category, false | ||
end | ||
|
||
# Boolean complement of `enabled` | ||
def disabled? | ||
!enabled | ||
end | ||
|
||
# @param [ManifoldEnv::Types::ThrottledCategory] name | ||
def disabled_for?(name) | ||
disabled? || category_disabled?(name) | ||
end | ||
|
||
# @see #toggle! | ||
# @param [ManifoldEnv::Types::ThrottledCategory] category | ||
# @return [void] | ||
def enable!(category) | ||
toggle! category, true | ||
end | ||
|
||
# @return [Terminal::Table] | ||
def status_table | ||
Terminal::Table.new(**STATUS_TABLE_OPTIONS) do |t| | ||
status_table_add_global!(t) | ||
|
||
status_table_add_categories!(t) | ||
end.tap do |t| | ||
status_table_align!(t) | ||
end | ||
end | ||
|
||
private | ||
|
||
# @param [Boolean] value | ||
# @return [String] | ||
def boolean_cell(value) | ||
value.present? ? ?T : ?F | ||
end | ||
|
||
# @param [Terminal::Table] t | ||
# @return [void] | ||
def status_table_add_categories!(table) | ||
ManifoldEnv::Types::THROTTLED_CATEGORIES.each do |category| | ||
table << [category.to_s.humanize, boolean_cell(__send__(category)), boolean_cell(!disabled_for?(category))] | ||
end | ||
end | ||
|
||
# @param [Terminal::Table] table | ||
# @return [void] | ||
def status_table_add_global!(table) | ||
table << ["All", boolean_cell(enabled?), boolean_cell(enabled?)] | ||
|
||
if disabled? | ||
table << :separator | ||
table << [{ value: "All Categories Disabled Globally", colspan: 3, alignment: :right }] | ||
end | ||
|
||
table << :separator | ||
end | ||
|
||
# @param [Terminal::Table] table | ||
# @return [void] | ||
def status_table_align!(table) | ||
table.align_column 1, :right | ||
table.align_column 2, :right | ||
end | ||
|
||
# @param [ManifoldEnv::Types::ThrottledCategory] category | ||
# @param [Boolean] value | ||
# @return [void] | ||
def toggle!(category, value) | ||
if category == :all | ||
self.enabled = value | ||
|
||
if value | ||
ManifoldEnv::Types::THROTTLED_CATEGORIES.each do |name| | ||
toggle!(name, value) | ||
end | ||
end | ||
elsif category?(category) | ||
self[category] = value | ||
else | ||
# :nocov: | ||
raise ArgumentError, "cannot toggle rate-limiting category #{category.inspect}" | ||
# :nocov: | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# frozen_string_literal: true | ||
|
||
Rails.application.configure do | ||
config.middleware.insert_before Rack::Attack, ManifoldEnv::Introspector | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 +1,60 @@ | ||
# frozen_string_literal: true | ||
|
||
require "auth_token" | ||
|
||
EMPTY_PARAMS = {}.with_indifferent_access.freeze | ||
|
||
JSON_PARAMS_FROM = ->(request) do | ||
params = JSON.parse(request.body) | ||
|
||
params.try(:with_indifferent_access) || EMPTY_PARAMS | ||
rescue JSON::ParserError | ||
EMPTY_PARAMS | ||
ensure | ||
request.body.rewind | ||
# :nocov: | ||
# We want to ensure that the public IP used by the client is never | ||
# accidentally blocklisted or throttled. | ||
unless Rails.env.development? || Rails.env.test? | ||
ManifoldEnv.rate_limiting.derive_public_ips! Rails.application.config.manifold.domain | ||
end | ||
|
||
IS_COMMENT_CREATE = ->(request) do | ||
request.post? && request.path.include?("/relationships/comments") | ||
ManifoldEnv.rate_limiting.public_ips.each do |public_ip| | ||
Rack::Attack.safelist_ip public_ip | ||
end | ||
# :nocov: | ||
|
||
IS_PUBLIC_ANNOTATION_CREATE = ->(request) do | ||
return false unless request.post? && request.path.include?("/relationships/annotations") | ||
|
||
params = JSON_PARAMS_FROM.(request) | ||
|
||
params.dig("data", "attributes", "private").blank? | ||
end | ||
|
||
IS_PUBLIC_RG_CREATE = ->(request) do | ||
return false unless request.post? && request.path.start_with?("/api/v1/reading_groups") | ||
|
||
params = JSON_PARAMS_FROM.(request) | ||
|
||
params.dig("data", "attributes", "privacy") != "private" | ||
Rack::Attack.safelist("allow all GET requests") do |request| | ||
# We do not currently throttle GET requests. | ||
request.get? | ||
end | ||
|
||
Rack::Attack.safelist("mark any admin access safe") do |request| | ||
AuthToken.authorized_admin?(request.env["HTTP_AUTHORIZATION"]) | ||
end | ||
|
||
ANN_LIMITS = { limit: 5, period: 300, }.freeze | ||
|
||
Rack::Attack.throttle("public annotation creation by email", **ANN_LIMITS) do |request| | ||
AuthToken.real_email_for(request.env["HTTP_AUTHORIZATION"]) if IS_PUBLIC_ANNOTATION_CREATE.(request) | ||
request.env["manifold_env.authorized_admin"] | ||
end | ||
|
||
Rack::Attack.throttle("public annotation creation by ip", **ANN_LIMITS) do |request| | ||
request.ip if IS_PUBLIC_ANNOTATION_CREATE.(request) | ||
Rack::Attack.safelist("skip when disabled globally or per category") do |request| | ||
request.env["manifold_env.rate_limiting_disabled"] | ||
end | ||
|
||
COMMENT_LIMITS = { limit: 10, period: 3600, }.freeze | ||
|
||
Rack::Attack.throttle("comment creation by email", **COMMENT_LIMITS) do |request| | ||
AuthToken.real_email_for(request.env["HTTP_AUTHORIZATION"]) if IS_COMMENT_CREATE.(request) | ||
end | ||
|
||
Rack::Attack.throttle("comment creation by ip", **COMMENT_LIMITS) do |request| | ||
request.ip if IS_COMMENT_CREATE.(request) | ||
end | ||
|
||
RG_LIMITS = { limit: 10, period: 3600, }.freeze | ||
|
||
Rack::Attack.throttle("public reading group creation by email", **RG_LIMITS) do |request| | ||
AuthToken.real_email_for(request.env["HTTP_AUTHORIZATION"]) if IS_PUBLIC_RG_CREATE.(request) | ||
end | ||
|
||
Rack::Attack.throttle("public reading group creation by ip", **RG_LIMITS) do |request| | ||
request.ip if IS_PUBLIC_RG_CREATE.(request) | ||
end | ||
|
||
Rack::Attack.blocklist("allow2ban registration by email") do |req| | ||
params = JSON_PARAMS_FROM.(req) | ||
|
||
real_email = AuthToken.real_email_from(params.dig("data", "attributes", "email")) | ||
|
||
Rack::Attack::Allow2Ban.filter(real_email, maxretry: 5, findtime: 1.day, bantime: 1.month) do | ||
req.path.start_with?("/api/v1/users") && req.post? | ||
ManifoldEnv.rate_limiting.each_throttled_category do |throttler| | ||
Rack::Attack.throttle throttler.email_key, **throttler.options do |request| | ||
request.env["manifold_env.real_email"] if request.env["manifold_env.throttled_category"] == throttler.category | ||
end | ||
end | ||
|
||
Rack::Attack.blocklist("allow2ban registration by ip") do |req| | ||
Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 5, findtime: 1.day, bantime: 1.month) do | ||
req.path.start_with?("/api/v1/users") && req.post? | ||
Rack::Attack.throttle throttler.ip_key, **throttler.options do |request| | ||
request.ip if request.env["manifold_env.throttled_category"] == throttler.category | ||
end | ||
end | ||
|
||
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |name, start, finish, request_id, payload| | ||
# :nocov: | ||
ThrottledRequest.track! payload[:request] | ||
# :nocov: | ||
end | ||
|
||
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish, request_id, payload| | ||
# :nocov: | ||
ThrottledRequest.track! payload[:request] | ||
# :nocov: | ||
end | ||
|
||
Rack::Attack.blocklisted_responder = lambda do |request| | ||
# :nocov: | ||
[503, {}, ["Internal Server Error\n"]] | ||
# :nocov: | ||
end | ||
|
||
Rack::Attack.throttled_responder = lambda do |request| | ||
# :nocov: | ||
[503, {}, ["Internal Server Error\n"]] | ||
# :nocov: | ||
end |
9 changes: 9 additions & 0 deletions
9
api/db/migrate/20240327194259_add_rate_limiting_to_settings.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# frozen_string_literal: true | ||
|
||
class AddRateLimitingToSettings < ActiveRecord::Migration[6.1] | ||
def change | ||
change_table :settings do |t| | ||
t.jsonb :rate_limiting, null: false, default: {} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.