Commit 17f70841 authored by Pavel Shutsin's avatar Pavel Shutsin

Unify request rate limiting logic

parent 83e70c2f
...@@ -23,6 +23,7 @@ class ApplicationController < ActionController::Base ...@@ -23,6 +23,7 @@ class ApplicationController < ActionController::Base
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include ::Gitlab::EndpointAttributes include ::Gitlab::EndpointAttributes
include FlocOptOut include FlocOptOut
include CheckRateLimit
before_action :authenticate_user!, except: [:route_not_found] before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
......
...@@ -5,19 +5,27 @@ ...@@ -5,19 +5,27 @@
# Controller concern that checks if the rate limit for a given action is throttled by calling the # Controller concern that checks if the rate limit for a given action is throttled by calling the
# Gitlab::ApplicationRateLimiter class. If the action is throttled for the current user, the request # Gitlab::ApplicationRateLimiter class. If the action is throttled for the current user, the request
# will be logged and an error message will be rendered with a Too Many Requests response status. # will be logged and an error message will be rendered with a Too Many Requests response status.
# See lib/api/helpers/rate_limiter.rb for API version
module CheckRateLimit module CheckRateLimit
def check_rate_limit(key) def check_rate_limit!(key, scope:, redirect_back: false, **options)
return unless rate_limiter.throttled?(key, scope: current_user, users_allowlist: rate_limit_users_allowlist) return unless rate_limiter.throttled?(key, scope: scope, **options)
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
return yield if block_given?
message = _('This endpoint has been requested too many times. Try again later.')
if redirect_back
redirect_back_or_default(options: { alert: message })
else
render plain: message, status: :too_many_requests
end
end end
private
def rate_limiter def rate_limiter
::Gitlab::ApplicationRateLimiter ::Gitlab::ApplicationRateLimiter
end end
def rate_limit_users_allowlist
Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
end
end end
...@@ -32,16 +32,4 @@ module Integrations::HooksExecution ...@@ -32,16 +32,4 @@ module Integrations::HooksExecution
flash[:alert] = "Hook execution failed: #{message}" flash[:alert] = "Hook execution failed: #{message}"
end end
end end
def create_rate_limit(key, scope)
if rate_limiter.throttled?(key, scope: [scope, current_user])
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end end
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
module NotesActions module NotesActions
include RendersNotes include RendersNotes
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include CheckRateLimit
extend ActiveSupport::Concern extend ActiveSupport::Concern
# last_fetched_at is an integer number of microseconds, which is the same # last_fetched_at is an integer number of microseconds, which is the same
...@@ -16,7 +15,11 @@ module NotesActions ...@@ -16,7 +15,11 @@ module NotesActions
before_action :require_noteable!, only: [:index, :create] before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create] before_action :note_project, only: [:create]
before_action -> { check_rate_limit(:notes_create) }, only: [:create] before_action -> {
check_rate_limit!(:notes_create,
scope: current_user,
users_allowlist: Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist)
}, only: [:create]
end end
def index def index
......
...@@ -37,7 +37,7 @@ class GroupsController < Groups::ApplicationController ...@@ -37,7 +37,7 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:iteration_cadences, @group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, @group, default_enabled: :yaml)
end end
before_action :export_rate_limit, only: [:export, :download_export] before_action :check_export_rate_limit!, only: [:export, :download_export]
helper_method :captcha_required? helper_method :captcha_required?
...@@ -314,16 +314,12 @@ class GroupsController < Groups::ApplicationController ...@@ -314,16 +314,12 @@ class GroupsController < Groups::ApplicationController
url_for(safe_params) url_for(safe_params)
end end
def export_rate_limit def check_export_rate_limit!
prefixed_action = "group_#{params[:action]}".to_sym prefixed_action = "group_#{params[:action]}".to_sym
scope = params[:action] == :download_export ? @group : nil scope = params[:action] == :download_export ? @group : nil
if Gitlab::ApplicationRateLimiter.throttled?(prefixed_action, scope: [current_user, scope].compact) check_rate_limit!(prefixed_action, scope: [current_user, scope].compact)
Gitlab::ApplicationRateLimiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end end
def ensure_export_enabled def ensure_export_enabled
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class Import::BaseController < ApplicationController class Import::BaseController < ApplicationController
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
before_action :import_rate_limit, only: [:create] before_action -> { check_rate_limit!(:project_import, scope: [current_user, :project_import], redirect_back: true) }, only: [:create]
feature_category :importers feature_category :importers
def status def status
...@@ -98,18 +98,4 @@ class Import::BaseController < ApplicationController ...@@ -98,18 +98,4 @@ class Import::BaseController < ApplicationController
def project_save_error(project) def project_save_error(project)
project.errors.full_messages.join(', ') project.errors.full_messages.join(', ')
end end
def import_rate_limit
key = "project_import".to_sym
if rate_limiter.throttled?(key, scope: [current_user, key])
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
redirect_back_or_default(options: { alert: _('This endpoint has been requested too many times. Try again later.') })
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end end
...@@ -4,7 +4,7 @@ class Import::GitlabGroupsController < ApplicationController ...@@ -4,7 +4,7 @@ class Import::GitlabGroupsController < ApplicationController
include WorkhorseAuthorization include WorkhorseAuthorization
before_action :ensure_group_import_enabled before_action :ensure_group_import_enabled
before_action :import_rate_limit, only: %i[create] before_action :check_import_rate_limit!, only: %i[create]
feature_category :importers feature_category :importers
...@@ -55,12 +55,9 @@ class Import::GitlabGroupsController < ApplicationController ...@@ -55,12 +55,9 @@ class Import::GitlabGroupsController < ApplicationController
render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true) render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true)
end end
def import_rate_limit def check_import_rate_limit!
if Gitlab::ApplicationRateLimiter.throttled?(:group_import, scope: current_user) check_rate_limit!(:group_import, scope: current_user) do
Gitlab::ApplicationRateLimiter.log_request(request, :group_import_request_limit, current_user) redirect_to new_group_path, alert: _('This endpoint has been requested too many times. Try again later.')
flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
redirect_to new_group_path
end end
end end
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
class Profiles::EmailsController < Profiles::ApplicationController class Profiles::EmailsController < Profiles::ApplicationController
before_action :find_email, only: [:destroy, :resend_confirmation_instructions] before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
before_action -> { rate_limit!(:profile_add_new_email) }, only: [:create] before_action -> { check_rate_limit!(:profile_add_new_email, scope: current_user, redirect_back: true) },
before_action -> { rate_limit!(:profile_resend_email_confirmation) }, only: [:resend_confirmation_instructions] only: [:create]
before_action -> { check_rate_limit!(:profile_resend_email_confirmation, scope: current_user, redirect_back: true) },
only: [:resend_confirmation_instructions]
feature_category :users feature_category :users
...@@ -42,16 +44,6 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -42,16 +44,6 @@ class Profiles::EmailsController < Profiles::ApplicationController
private private
def rate_limit!(action)
rate_limiter = ::Gitlab::ApplicationRateLimiter
if rate_limiter.throttled?(action, scope: current_user)
rate_limiter.log_request(request, action, current_user)
redirect_back_or_default(options: { alert: _('This action has been performed too many times. Try again later.') })
end
end
def email_params def email_params
params.require(:email).permit(:email) params.require(:email).permit(:email)
end end
......
...@@ -6,7 +6,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::HooksController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :hook_logs, only: :edit before_action :hook_logs, only: :edit
before_action -> { create_rate_limit(:project_testing_hook, @project) }, only: :test before_action -> { check_rate_limit!(:project_testing_hook, scope: [@project, current_user]) }, only: :test
respond_to :html respond_to :html
......
...@@ -37,7 +37,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -37,7 +37,9 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches] before_action :authorize_download_code!, only: [:related_branches]
# Limit the amount of issues created per minute # Limit the amount of issues created per minute
before_action :create_rate_limit, only: [:create], if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) } before_action -> { check_rate_limit!(:issues_create, scope: [@project, @current_user])},
only: [:create],
if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) }
before_action do before_action do
push_frontend_feature_flag(:tribute_autocomplete, @project) push_frontend_feature_flag(:tribute_autocomplete, @project)
...@@ -363,20 +365,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -363,20 +365,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_compare_path(project, from: project.default_branch, to: branch[:name]) project_compare_path(project, from: project.default_branch, to: branch[:name])
end end
def create_rate_limit
key = :issues_create
if rate_limiter.throttled?(key, scope: [@project, @current_user])
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
def service_desk? def service_desk?
action_name == 'service_desk' action_name == 'service_desk'
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class Projects::PipelineSchedulesController < Projects::ApplicationController class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create] before_action :schedule, except: [:index, :new, :create]
before_action :play_rate_limit, only: [:play] before_action :check_play_rate_limit!, only: [:play]
before_action :authorize_play_pipeline_schedule!, only: [:play] before_action :authorize_play_pipeline_schedule!, only: [:play]
before_action :authorize_read_pipeline_schedule! before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create] before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
...@@ -81,19 +81,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController ...@@ -81,19 +81,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
private private
def play_rate_limit def check_play_rate_limit!
return unless current_user return unless current_user
if rate_limiter.throttled?(:play_pipeline_schedule, scope: [current_user, schedule]) check_rate_limit!(:play_pipeline_schedule, scope: [current_user, schedule]) do
flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.') flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.')
redirect_to pipeline_schedules_path(@project) redirect_to pipeline_schedules_path(@project)
end end
end end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
def schedule def schedule
@schedule ||= project.pipeline_schedules.find(params[:id]) @schedule ||= project.pipeline_schedules.find(params[:id])
end end
......
...@@ -13,7 +13,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -13,7 +13,7 @@ class Projects::RawController < Projects::ApplicationController
before_action :set_ref_and_path before_action :set_ref_and_path
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :show_rate_limit, only: [:show], unless: :external_storage_request? before_action :check_show_rate_limit!, only: [:show], unless: :external_storage_request?
before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled? before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
feature_category :source_code_management feature_category :source_code_management
...@@ -33,23 +33,11 @@ class Projects::RawController < Projects::ApplicationController ...@@ -33,23 +33,11 @@ class Projects::RawController < Projects::ApplicationController
@ref, @path = extract_ref(get_id) @ref, @path = extract_ref(get_id)
end end
def show_rate_limit def check_show_rate_limit!
if rate_limiter.throttled?(:show_raw_controller, scope: [@project, @path], threshold: raw_blob_request_limit) check_rate_limit!(:raw_blob, scope: [@project, @path]) do
rate_limiter.log_request(request, :raw_blob_request_limit, current_user)
render plain: _('You cannot access the raw file. Please wait a minute.'), status: :too_many_requests render plain: _('You cannot access the raw file. Please wait a minute.'), status: :too_many_requests
end end
end end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
def raw_blob_request_limit
Gitlab::CurrentSettings
.current_application_settings
.raw_blob_request_limit
end
end end
Projects::RawController.prepend_mod Projects::RawController.prepend_mod
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
class Projects::RepositoriesController < Projects::ApplicationController class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include StaticObjectExternalStorage include StaticObjectExternalStorage
include Gitlab::RateLimitHelpers
include HotlinkInterceptor include HotlinkInterceptor
include Gitlab::RepositoryArchiveRateLimiter
prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) } prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }
skip_before_action :default_cache_headers, only: :archive skip_before_action :default_cache_headers, only: :archive
# Authorize # Authorize
before_action :check_archive_rate_limiting!, only: :archive
before_action :require_non_empty_project, except: :create before_action :require_non_empty_project, except: :create
before_action :archive_rate_limit!, only: :archive
before_action :intercept_hotlinking!, only: :archive before_action :intercept_hotlinking!, only: :archive
before_action :assign_archive_vars, only: :archive before_action :assign_archive_vars, only: :archive
before_action :assign_append_sha, only: :archive before_action :assign_append_sha, only: :archive
...@@ -42,12 +42,6 @@ class Projects::RepositoriesController < Projects::ApplicationController ...@@ -42,12 +42,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
private private
def archive_rate_limit!
if archive_rate_limit_reached?(current_user, @project)
render plain: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE, status: :too_many_requests
end
end
def repo_params def repo_params
@repo_params ||= { ref: @ref, path: params[:path], format: params[:format], append_sha: @append_sha } @repo_params ||= { ref: @ref, path: params[:path], format: params[:format], append_sha: @append_sha }
end end
...@@ -125,6 +119,12 @@ class Projects::RepositoriesController < Projects::ApplicationController ...@@ -125,6 +119,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
[path, nil] [path, nil]
end end
end end
def check_archive_rate_limiting!
check_archive_rate_limit!(current_user, @project) do
render(plain: _('This archive has been requested too many times. Try again later.'), status: :too_many_requests)
end
end
end end
Projects::RepositoriesController.prepend_mod_with('Projects::RepositoriesController') Projects::RepositoriesController.prepend_mod_with('Projects::RepositoriesController')
...@@ -30,7 +30,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -30,7 +30,7 @@ class ProjectsController < Projects::ApplicationController
before_action :event_filter, only: [:show, :activity] before_action :event_filter, only: [:show, :activity]
# Project Export Rate Limit # Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export] before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do before_action do
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml) push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
...@@ -544,20 +544,12 @@ class ProjectsController < Projects::ApplicationController ...@@ -544,20 +544,12 @@ class ProjectsController < Projects::ApplicationController
@project = @project.present(current_user: current_user) @project = @project.present(current_user: current_user)
end end
def export_rate_limit def check_export_rate_limit!
prefixed_action = "project_#{params[:action]}".to_sym prefixed_action = "project_#{params[:action]}".to_sym
project_scope = params[:action] == 'download_export' ? @project : nil project_scope = params[:action] == 'download_export' ? @project : nil
if rate_limiter.throttled?(prefixed_action, scope: [current_user, project_scope].compact) check_rate_limit!(prefixed_action, scope: [current_user, project_scope].compact)
rate_limiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end end
def render_edit def render_edit
......
...@@ -4,7 +4,7 @@ class Admin::EmailsController < Admin::ApplicationController ...@@ -4,7 +4,7 @@ class Admin::EmailsController < Admin::ApplicationController
include Admin::EmailsHelper include Admin::EmailsHelper
before_action :check_license_send_emails_from_admin_area_available! before_action :check_license_send_emails_from_admin_area_available!
before_action :check_rate_limit!, only: [:create] before_action :check_emails_rate_limit!, only: [:create]
feature_category :not_owned feature_category :not_owned
...@@ -18,7 +18,7 @@ class Admin::EmailsController < Admin::ApplicationController ...@@ -18,7 +18,7 @@ class Admin::EmailsController < Admin::ApplicationController
private private
def check_rate_limit! def check_emails_rate_limit!
if admin_emails_are_currently_rate_limited? if admin_emails_are_currently_rate_limited?
redirect_to admin_email_path, alert: _('Email could not be sent') redirect_to admin_email_path, alert: _('Email could not be sent')
end end
......
...@@ -17,8 +17,8 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -17,8 +17,8 @@ class Groups::EpicsController < Groups::ApplicationController
before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update] before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update]
after_action :log_epic_show, only: :show after_action :log_epic_show, only: :show
# Limit the amount of epics created per minute # Limit the amount of epics created per minute. Epics share the issue creation rate limit
before_action :create_rate_limit, only: [:create] before_action -> { check_rate_limit!(:issues_create, scope: current_user) }, only: [:create]
before_action do before_action do
push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml)
...@@ -133,19 +133,4 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -133,19 +133,4 @@ class Groups::EpicsController < Groups::ApplicationController
def verify_group_bulk_edit_enabled! def verify_group_bulk_edit_enabled!
render_404 unless group.licensed_feature_available?(:group_bulk_edit) render_404 unless group.licensed_feature_available?(:group_bulk_edit)
end end
def create_rate_limit
# Epics share the issue creation rate limit
key = :issues_create
if rate_limiter.throttled?(key, scope: current_user)
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end end
...@@ -8,7 +8,7 @@ class Groups::HooksController < Groups::ApplicationController ...@@ -8,7 +8,7 @@ class Groups::HooksController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :check_group_webhooks_available! before_action :check_group_webhooks_available!
before_action :set_hook, only: [:edit, :update, :test, :destroy] before_action :set_hook, only: [:edit, :update, :test, :destroy]
before_action -> { create_rate_limit(:group_testing_hook, @group) }, only: :test before_action -> { check_rate_limit!(:group_testing_hook, scope: [@group, current_user]) }, only: :test
respond_to :html respond_to :html
......
...@@ -18,7 +18,7 @@ module API ...@@ -18,7 +18,7 @@ module API
detail 'This feature was introduced in GitLab 12.5.' detail 'This feature was introduced in GitLab 12.5.'
end end
get ':id/export/download' do get ':id/export/download' do
check_rate_limit! :group_download_export, [current_user, user_group] check_rate_limit! :group_download_export, scope: [current_user, user_group]
if user_group.export_file_exists? if user_group.export_file_exists?
if user_group.export_archive_exists? if user_group.export_archive_exists?
...@@ -35,7 +35,7 @@ module API ...@@ -35,7 +35,7 @@ module API
detail 'This feature was introduced in GitLab 12.5.' detail 'This feature was introduced in GitLab 12.5.'
end end
post ':id/export' do post ':id/export' do
check_rate_limit! :group_export, [current_user] check_rate_limit! :group_export, scope: current_user
export_service = ::Groups::ImportExport::ExportService.new(group: user_group, user: current_user) export_service = ::Groups::ImportExport::ExportService.new(group: user_group, user: current_user)
......
...@@ -2,26 +2,27 @@ ...@@ -2,26 +2,27 @@
module API module API
module Helpers module Helpers
# == RateLimiter
#
# Helper that checks if the rate limit for a given endpoint is throttled by calling the
# Gitlab::ApplicationRateLimiter class. If the action is throttled for the current user, the request
# will be logged and an error message will be rendered with a Too Many Requests response status.
# See app/controllers/concerns/check_rate_limit.rb for Rails controllers version
module RateLimiter module RateLimiter
def check_rate_limit!(key, scope, users_allowlist = nil) def check_rate_limit!(key, scope:, **options)
if rate_limiter.throttled?(key, scope: scope, users_allowlist: users_allowlist) return unless rate_limiter.throttled?(key, scope: scope, **options)
log_request(key)
render_exceeded_limit_error!
end
end
private rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
def rate_limiter return yield if block_given?
::Gitlab::ApplicationRateLimiter
end
def render_exceeded_limit_error!
render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429) render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429)
end end
def log_request(key) private
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
def rate_limiter
::Gitlab::ApplicationRateLimiter
end end
end end
end end
......
...@@ -262,7 +262,7 @@ module API ...@@ -262,7 +262,7 @@ module API
post ':id/issues' do post ':id/issues' do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21140') Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21140')
check_rate_limit! :issues_create, [current_user] if Feature.disabled?("rate_limited_service_issues_create", user_project, default_enabled: :yaml) check_rate_limit!(:issues_create, scope: current_user) if Feature.disabled?("rate_limited_service_issues_create", user_project, default_enabled: :yaml)
authorize! :create_issue, user_project authorize! :create_issue, user_project
......
...@@ -79,7 +79,7 @@ module API ...@@ -79,7 +79,7 @@ module API
post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do
allowlist = allowlist =
Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
check_rate_limit! :notes_create, [current_user], allowlist check_rate_limit! :notes_create, scope: current_user, users_allowlist: allowlist
noteable = find_noteable(noteable_type, params[:noteable_id]) noteable = find_noteable(noteable_type, params[:noteable_id])
opts = { opts = {
......
...@@ -25,7 +25,7 @@ module API ...@@ -25,7 +25,7 @@ module API
detail 'This feature was introduced in GitLab 10.6.' detail 'This feature was introduced in GitLab 10.6.'
end end
get ':id/export/download' do get ':id/export/download' do
check_rate_limit! :project_download_export, [current_user, user_project] check_rate_limit! :project_download_export, scope: [current_user, user_project]
if user_project.export_file_exists? if user_project.export_file_exists?
if user_project.export_archive_exists? if user_project.export_archive_exists?
...@@ -49,7 +49,7 @@ module API ...@@ -49,7 +49,7 @@ module API
end end
end end
post ':id/export' do post ':id/export' do
check_rate_limit! :project_export, [current_user] check_rate_limit! :project_export, scope: current_user
user_project.remove_exports user_project.remove_exports
......
...@@ -81,7 +81,7 @@ module API ...@@ -81,7 +81,7 @@ module API
post 'import' do post 'import' do
require_gitlab_workhorse! require_gitlab_workhorse!
check_rate_limit! :project_import, [current_user, :project_import] check_rate_limit! :project_import, scope: [current_user, :project_import]
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21041') Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21041')
...@@ -135,7 +135,7 @@ module API ...@@ -135,7 +135,7 @@ module API
post 'remote-import' do post 'remote-import' do
not_found! unless ::Feature.enabled?(:import_project_from_remote_file) not_found! unless ::Feature.enabled?(:import_project_from_remote_file)
check_rate_limit! :project_import, [current_user, :project_import] check_rate_limit! :project_import, scope: [current_user, :project_import]
response = ::Import::GitlabProjects::CreateProjectFromRemoteFileService.new( response = ::Import::GitlabProjects::CreateProjectFromRemoteFileService.new(
current_user, current_user,
......
...@@ -45,7 +45,7 @@ module API ...@@ -45,7 +45,7 @@ module API
end end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do helpers do
include ::Gitlab::RateLimitHelpers include Gitlab::RepositoryArchiveRateLimiter
def handle_project_member_errors(errors) def handle_project_member_errors(errors)
if errors[:project_access].any? if errors[:project_access].any?
...@@ -150,8 +150,8 @@ module API ...@@ -150,8 +150,8 @@ module API
optional :path, type: String, desc: 'Subfolder of the repository to be downloaded' optional :path, type: String, desc: 'Subfolder of the repository to be downloaded'
end end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
if archive_rate_limit_reached?(current_user, user_project) check_archive_rate_limit!(current_user, user_project) do
render_api_error!({ error: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE }, 429) render_api_error!({ error: _('This archive has been requested too many times. Try again later.') }, 429)
end end
not_acceptable! if Gitlab::HotlinkingDetector.intercept_hotlinking?(request) not_acceptable! if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
......
...@@ -4,12 +4,7 @@ module Gitlab ...@@ -4,12 +4,7 @@ module Gitlab
# This class implements a simple rate limiter that can be used to throttle # This class implements a simple rate limiter that can be used to throttle
# certain actions. Unlike Rack Attack and Rack::Throttle, which operate at # certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
# the middleware level, this can be used at the controller or API level. # the middleware level, this can be used at the controller or API level.
# # See CheckRateLimit concern for usage.
# @example
# if Gitlab::ApplicationRateLimiter.throttled?(:project_export, scope: [@project, @current_user])
# flash[:alert] = 'error!'
# redirect_to(edit_project_path(@project), status: :too_many_requests)
# end
class ApplicationRateLimiter class ApplicationRateLimiter
InvalidKeyError = Class.new(StandardError) InvalidKeyError = Class.new(StandardError)
...@@ -47,7 +42,7 @@ module Gitlab ...@@ -47,7 +42,7 @@ module Gitlab
project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute }, project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute },
project_testing_hook: { threshold: 5, interval: 1.minute }, project_testing_hook: { threshold: 5, interval: 1.minute },
play_pipeline_schedule: { threshold: 1, interval: 1.minute }, play_pipeline_schedule: { threshold: 1, interval: 1.minute },
show_raw_controller: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute }, raw_blob: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute },
group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute }, group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute }, group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute }, group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
......
# frozen_string_literal: true
module Gitlab
module RateLimitHelpers
ARCHIVE_RATE_LIMIT_REACHED_MESSAGE = 'This archive has been requested too many times. Try again later.'
ARCHIVE_RATE_ANONYMOUS_THRESHOLD = 100 # Allow 100 requests/min for anonymous users
ARCHIVE_RATE_THROTTLE_KEY = :project_repositories_archive
def archive_rate_limit_reached?(user, project)
return false unless Feature.enabled?(:archive_rate_limit)
key = ARCHIVE_RATE_THROTTLE_KEY
if rate_limiter.throttled?(key, scope: [project, user], threshold: archive_rate_threshold_by_user(user))
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, user)
return true
end
false
end
def archive_rate_threshold_by_user(user)
if user
nil # Use the defaults
else
ARCHIVE_RATE_ANONYMOUS_THRESHOLD
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end
end
# frozen_string_literal: true
module Gitlab
module RepositoryArchiveRateLimiter
def check_archive_rate_limit!(current_user, project, &block)
return unless Feature.enabled?(:archive_rate_limit)
threshold = current_user ? nil : 100
check_rate_limit!(:project_repositories_archive, scope: [project, current_user], threshold: threshold, &block)
end
end
end
...@@ -35576,9 +35576,6 @@ msgstr "" ...@@ -35576,9 +35576,6 @@ msgstr ""
msgid "This action cannot be undone, and will permanently delete the %{key} SSH key" msgid "This action cannot be undone, and will permanently delete the %{key} SSH key"
msgstr "" msgstr ""
msgid "This action has been performed too many times. Try again later."
msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all related resources, including issues and merge requests." msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all related resources, including issues and merge requests."
msgstr "" msgstr ""
...@@ -35603,6 +35600,9 @@ msgstr "" ...@@ -35603,6 +35600,9 @@ msgstr ""
msgid "This application will be able to:" msgid "This application will be able to:"
msgstr "" msgstr ""
msgid "This archive has been requested too many times. Try again later."
msgstr ""
msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}." msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}."
msgstr "" msgstr ""
......
...@@ -33,7 +33,7 @@ RSpec.describe Profiles::EmailsController do ...@@ -33,7 +33,7 @@ RSpec.describe Profiles::EmailsController do
subject subject
expect(response).to have_gitlab_http_status(:redirect) expect(response).to have_gitlab_http_status(:redirect)
expect(flash[:alert]).to eq(_('This action has been performed too many times. Try again later.')) expect(flash[:alert]).to eq(_('This endpoint has been requested too many times. Try again later.'))
end end
end end
end end
......
...@@ -86,7 +86,7 @@ RSpec.describe Projects::RepositoriesController do ...@@ -86,7 +86,7 @@ RSpec.describe Projects::RepositoriesController do
describe 'rate limiting' do describe 'rate limiting' do
it 'rate limits user when thresholds hit' do it 'rate limits user when thresholds hit' do
expect(controller).to receive(:archive_rate_limit_reached?).and_return(true) allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: "html" get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: "html"
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::RateLimitHelpers, :clean_gitlab_redis_rate_limiting do
let(:limiter_class) do
Class.new do
include ::Gitlab::RateLimitHelpers
attr_reader :request
def initialize(request)
@request = request
end
end
end
let(:request) { instance_double(ActionDispatch::Request, request_method: 'GET', ip: '127.0.0.1', fullpath: '/') }
let(:class_instance) { limiter_class.new(request) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
describe '#archive_rate_limit_reached?' do
context 'with a user' do
it 'rate limits the user properly' do
5.times do
expect(class_instance.archive_rate_limit_reached?(user, project)).to be_falsey
end
expect(class_instance.archive_rate_limit_reached?(user, project)).to be_truthy
end
end
context 'with an anonymous user' do
before do
stub_const('Gitlab::RateLimitHelpers::ARCHIVE_RATE_ANONYMOUS_THRESHOLD', 2)
end
it 'rate limits with higher limits' do
2.times do
expect(class_instance.archive_rate_limit_reached?(nil, project)).to be_falsey
end
expect(class_instance.archive_rate_limit_reached?(nil, project)).to be_truthy
expect(class_instance.archive_rate_limit_reached?(user, project)).to be_falsey
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::RepositoryArchiveRateLimiter do
let(:described_class) do
Class.new do
include ::Gitlab::RepositoryArchiveRateLimiter
def check_rate_limit!(**args)
end
end
end
describe "#check_archive_rate_limit!" do
let(:project) { instance_double('Project') }
let(:current_user) { instance_double('User') }
let(:check) { subject.check_archive_rate_limit!(current_user, project) }
context 'when archive_rate_limit feature flag is disabled' do
before do
stub_feature_flags(archive_rate_limit: false)
end
it 'does not check rate limit' do
expect(subject).not_to receive(:check_rate_limit!)
expect(check).to eq nil
end
end
context 'when archive_rate_limit feature flag is enabled' do
before do
stub_feature_flags(archive_rate_limit: true)
end
context 'when current user exists' do
it 'checks for project_repositories_archive rate limiting with default threshold' do
expect(subject).to receive(:check_rate_limit!)
.with(:project_repositories_archive, scope: [project, current_user], threshold: nil)
check
end
end
context 'when current user does not exist' do
let(:current_user) { nil }
it 'checks for project_repositories_archive rate limiting with threshold 100' do
expect(subject).to receive(:check_rate_limit!)
.with(:project_repositories_archive, scope: [project, current_user], threshold: 100)
check
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment