Skip to content

Commit

Permalink
Merge pull request #2028 from internetee/1017-rate-throttling
Browse files Browse the repository at this point in the history
Add request throttling
  • Loading branch information
vohmar authored Oct 28, 2022
2 parents ded19f8 + 0b76149 commit 517fac6
Show file tree
Hide file tree
Showing 74 changed files with 1,638 additions and 20 deletions.
15 changes: 15 additions & 0 deletions app/controllers/epp/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ class AuthorizationError < StandardError; end

rescue_from StandardError, with: :respond_with_command_failed_error
rescue_from AuthorizationError, with: :respond_with_authorization_error
rescue_from Shunter::ThrottleError, with: :respond_with_session_limit_exceeded_error
rescue_from ActiveRecord::RecordNotFound, with: :respond_with_object_does_not_exist_error

before_action :set_paper_trail_whodunnit

skip_before_action :validate_against_schema

protected

def respond_with_session_limit_exceeded_error(exception)
epp_errors.add(:epp_errors,
code: '2502',
msg: Shunter.default_error_message)
handle_errors
log_exception(exception)
end

def respond_with_command_failed_error(exception)
epp_errors.add(:epp_errors,
code: '2400',
Expand All @@ -51,6 +61,11 @@ def respond_with_authorization_error

private

def throttled_user
authorize!(:throttled_user, @domain) unless current_user || instance_of?(Epp::SessionsController)
current_user
end

def wrap_exceptions
yield
rescue CanCan::AccessDenied
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/epp/contacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class ContactsController < BaseController
before_action :find_contact, only: [:info, :update, :delete]
before_action :find_password, only: [:info, :update, :delete]

THROTTLED_ACTIONS = %i[info check create renew update transfer delete].freeze
include Shunter::Integration::Throttle

def info
authorize! :info, @contact, @password
render_epp_response 'epp/contacts/info'
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/epp/domains_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class DomainsController < BaseController
before_action :set_paper_trail_whodunnit
before_action :parse_schemas_prefix_and_version

THROTTLED_ACTIONS = %i[info create check renew update transfer delete].freeze
include Shunter::Integration::Throttle

def info
authorize! :info, @domain

Expand Down
3 changes: 3 additions & 0 deletions app/controllers/epp/polls_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module Epp
class PollsController < BaseController
THROTTLED_ACTIONS = %i[poll].freeze
include Shunter::Integration::Throttle

def poll
authorize! :manage, :poll
req_poll if params[:parsed_frame].css('poll').first['op'] == 'req'
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/epp/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ class SessionsController < BaseController
skip_authorization_check only: [:hello, :login, :logout]
before_action :set_paper_trail_whodunnit

THROTTLED_ACTIONS = %i[login hello].freeze
include Shunter::Integration::Throttle

def hello
render_epp_response('greeting')
end
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/repp/v1/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ module V1
class AccountsController < BaseController # rubocop:disable Metrics/ClassLength
load_and_authorize_resource

THROTTLED_ACTIONS = %i[
index balance details update_auto_reload_balance disable_auto_reload_balance switch_user update
].freeze
include Shunter::Integration::Throttle

api :get, '/repp/v1/accounts'
desc 'Get all activities'
def index
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/repp/v1/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def log_request
@response = { code: 2201, message: 'Authorization error' }
logger.error e.to_s
render(json: @response, status: :unauthorized)
rescue Shunter::ThrottleError => e
@response = { code: 2502, message: Shunter.default_error_message }
logger.error e.to_s
render(json: @response, status: :bad_request)
ensure
create_repp_log
end
Expand Down Expand Up @@ -167,6 +171,11 @@ def auth_values_to_data(registrar:)
data[:abilities] = Ability.new(current_user).permissions
data
end

def throttled_user
authorize!(:throttled_user, @domain) unless current_user || action_name == 'tara_callback'
current_user
end
end
end
end
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/contacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class ContactsController < BaseController # rubocop:disable Metrics/ClassLength
before_action :find_contact, only: %i[show update destroy]
skip_around_action :log_request, only: :search

THROTTLED_ACTIONS = %i[index check search create show update destroy].freeze
include Shunter::Integration::Throttle

api :get, '/repp/v1/contacts'
desc 'Get all existing contacts'
def index
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/admin_contacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module Repp
module V1
module Domains
class AdminContactsController < BaseContactsController
THROTTLED_ACTIONS = %i[update].freeze
include Shunter::Integration::Throttle

def update
super

Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/contacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Domains
class ContactsController < BaseContactsController
before_action :set_domain, only: %i[index create destroy]

THROTTLED_ACTIONS = %i[index create destroy update].freeze
include Shunter::Integration::Throttle

def_param_group :contacts_apidoc do
param :contacts, Array, required: true, desc: 'Array of new linked contacts' do
param :code, String, required: true, desc: 'Contact code'
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/dnssec_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Domains
class DnssecController < BaseController
before_action :set_domain, only: %i[index create destroy]

THROTTLED_ACTIONS = %i[index create destroy].freeze
include Shunter::Integration::Throttle

def_param_group :dns_keys_apidoc do
param :flags, String, required: true, desc: '256 (KSK) or 257 (ZSK)'
param :protocol, String, required: true, desc: 'Key protocol (3)'
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/nameservers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class NameserversController < BaseController
before_action :set_domain, only: %i[index create destroy]
before_action :set_nameserver, only: %i[destroy]

THROTTLED_ACTIONS = %i[index create destroy].freeze
include Shunter::Integration::Throttle

api :GET, '/repp/v1/domains/:domain_name/nameservers'
desc "Get domain's nameservers"
def index
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/renews_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class RenewsController < BaseController
before_action :select_renewable_domains, only: [:bulk_renew]
before_action :set_domain, only: [:create]

THROTTLED_ACTIONS = %i[create bulk_renew].freeze
include Shunter::Integration::Throttle

api :POST, 'repp/v1/domains/:domain_name/renew'
desc 'Renew domain'
param :renews, Hash, required: true, desc: 'Renew parameters' do
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class StatusesController < BaseController
before_action :set_domain, only: %i[update destroy]
before_action :verify_status

THROTTLED_ACTIONS = %i[update destroy].freeze
include Shunter::Integration::Throttle

api :DELETE, '/repp/v1/domains/:domain_name/statuses/:status'
param :domain_name, String, desc: 'Domain name'
desc 'Remove status from specific domain'
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains/transfers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Domains
class TransfersController < BaseController
before_action :set_domain, only: [:create]

THROTTLED_ACTIONS = %i[create].freeze
include Shunter::Integration::Throttle

api :POST, 'repp/v1/domains/:domain_name/transfer'
desc 'Transfer a specific domain'
param :transfer, Hash, required: true, desc: 'Renew parameters' do
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/domains_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class DomainsController < BaseController # rubocop:disable Metrics/ClassLength
before_action :forward_registrar_id, only: %i[create update destroy]
before_action :set_domain, only: %i[update]

THROTTLED_ACTIONS = %i[transfer_info transfer index create show update destroy].freeze
include Shunter::Integration::Throttle

api :GET, '/repp/v1/domains'
desc 'Get all existing domains'
def index
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/invoices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module V1
class InvoicesController < BaseController # rubocop:disable Metrics/ClassLength
load_and_authorize_resource

THROTTLED_ACTIONS = %i[download add_credit send_to_recipient cancel index show].freeze
include Shunter::Integration::Throttle

# rubocop:disable Metrics/MethodLength
api :get, '/repp/v1/invoices'
desc 'Get all invoices'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ module V1
module Registrar
class AccreditationInfoController < BaseController
if Feature.allow_accr_endspoints?
api :GET, 'repp/v1/registrar/accreditation/get_info'
desc 'check login user and return data'
THROTTLED_ACTIONS = %i[index].freeze
include Shunter::Integration::Throttle

api :GET, 'repp/v1/registrar/accreditation/get_info'
desc 'check login user and return data'

def index
login = current_user
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/registrar/auth_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class AuthController < BaseController
skip_before_action :check_ip_restriction, only: :tara_callback
skip_before_action :validate_client_certs, only: :tara_callback

THROTTLED_ACTIONS = %i[index tara_callback].freeze
include Shunter::Integration::Throttle

api :GET, 'repp/v1/registrar/auth'
desc 'check user auth info and return data'
def index
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/registrar/nameservers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Registrar
class NameserversController < BaseController
before_action :verify_nameserver_existance, only: %i[update]

THROTTLED_ACTIONS = %i[put].freeze
include Shunter::Integration::Throttle

api :PUT, 'repp/v1/registrar/nameservers'
desc 'bulk nameserver change'
param :data, Hash, required: true, desc: 'Object holding nameserver changes' do
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/registrar/notifications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Registrar
class NotificationsController < BaseController
before_action :set_notification, only: %i[update show]

THROTTLED_ACTIONS = %i[all_notifications index show update].freeze
include Shunter::Integration::Throttle

api :GET, '/repp/v1/registrar/notifications'
desc 'Get the latest unread poll message'
def index
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repp/v1/registrar/summary_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module Repp
module V1
module Registrar
class SummaryController < BaseController
THROTTLED_ACTIONS = %i[index].freeze
include Shunter::Integration::Throttle

api :GET, 'repp/v1/registrar/summary'
desc 'check user summary info and return data'

Expand Down
36 changes: 36 additions & 0 deletions app/lib/shunter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Shunter
module_function

class ThrottleError < StandardError; end

BASE_LOGGER = ::Logger.new($stdout)
ONE_MINUTE = 60
ONE_HUNDRED_REQUESTS = 100

BASE_CONNECTION = {
host: ENV['shunter_redis_host'] || 'redis',
port: (ENV['shunter_redis_port'] || '6379').to_i,
}.freeze

def default_error_message
"Session limit exceeded. Current limit is #{default_threshold} in #{default_timespan} seconds"
end

def default_timespan
ENV['shunter_default_timespan'] || ONE_MINUTE
end

def default_threshold
ENV['shunter_default_threshold'] || ONE_HUNDRED_REQUESTS
end

def default_adapter
ENV['shunter_default_adapter'] || 'Shunter::Adapters::Redis'
end

def feature_enabled?
ActiveModel::Type::Boolean.new.cast(ENV['shunter_enabled'] || 'false')
end
end
31 changes: 31 additions & 0 deletions app/lib/shunter/adapters/memory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Shunter
module Adapters
class Memory
attr_reader :store

def initialize(_options = {})
@@store ||= {}
end

def find_counter(key)
@@store[key]
end

def write_counter(key)
@@store[key] = 1
end

def increment_counter(key)
@@store[key] += 1
end

def clear!
@@store = {}
end

def expire_counter(_key, _timespan); end
end
end
end
29 changes: 29 additions & 0 deletions app/lib/shunter/adapters/redis.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Shunter
module Adapters
class Redis
attr_reader :redis

def initialize(options)
@redis = ::Redis.new(options)
end

def find_counter(key)
@redis.get(key)
end

def write_counter(key)
@redis.set(key, 1)
end

def increment_counter(key)
@redis.incr(key)
end

def expire_counter(key, timespan)
@redis.expire(key, timespan)
end
end
end
end
Loading

0 comments on commit 517fac6

Please sign in to comment.