diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 1c2bd10bc8143dd979ed9a997aef4b429f405d7c..3536205daeacb652d7e0e84bb6c2ca870f901b00 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -6,7 +6,8 @@ class AutocompleteController < ApplicationController skip_before_action :authenticate_user!, only: [ :users, :award_emojis, :merge_request_target_branches, :merge_request_source_branches ] - before_action :check_search_rate_limit!, only: [:users, :projects] + before_action :check_search_rate_limit!, only: :projects + before_action :check_autocomplete_users_rate_limit!, only: :users feature_category :user_profile, [:users, :user] feature_category :groups_and_projects, [:projects] @@ -112,6 +113,16 @@ def merge_request_branches(source: false, target: false) render json: { error: _('At least one of group_id or project_id must be specified') }, status: :bad_request end end + + def check_autocomplete_users_rate_limit! + return check_search_rate_limit! if Feature.disabled?(:autocomplete_users_rate_limit) # rubocop:disable Gitlab/FeatureFlagWithoutActor -- cannot scope to user as it needs to handle unauthenticated requests + + if current_user + check_rate_limit!(:autocomplete_users, scope: current_user) + else + check_rate_limit!(:autocomplete_users_unauthenticated, scope: request.ip) + end + end end AutocompleteController.prepend_mod_with('AutocompleteController') diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 4db587b6649ae485ee5fd6bcacba14513abea881..ceb3e38668622b838b425ada839aaca43b8b7064 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -258,6 +258,8 @@ def visible_attributes :authorized_keys_enabled, :auto_devops_enabled, :auto_devops_domain, + :autocomplete_users_limit, + :autocomplete_users_unauthenticated_limit, :concurrent_github_import_jobs_limit, :concurrent_bitbucket_import_jobs_limit, :concurrent_bitbucket_server_import_jobs_limit, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 17adf898ff36630d82e878aadb7bc65c995e941e..c5f9efb577526e20302b6230dbcebec085948fa9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -573,6 +573,8 @@ def self.kroki_formats_attributes with_options(numericality: { only_integer: true, greater_than: 0 }) do validates :ai_action_api_rate_limit, + :autocomplete_users_limit, + :autocomplete_users_unauthenticated_limit, :bulk_import_concurrent_pipeline_batch_limit, :code_suggestions_api_rate_limit, :concurrent_bitbucket_import_jobs_limit, @@ -679,6 +681,8 @@ def self.kroki_formats_attributes validates :resource_usage_limits, json_schema: { filename: 'resource_usage_limits' } jsonb_accessor :rate_limits, + autocomplete_users_limit: [:integer, { default: 300 }], + autocomplete_users_unauthenticated_limit: [:integer, { default: 100 }], concurrent_bitbucket_import_jobs_limit: [:integer, { default: 100 }], concurrent_bitbucket_server_import_jobs_limit: [:integer, { default: 100 }], concurrent_github_import_jobs_limit: [:integer, { default: 1000 }], diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index de0f6de43af87130010cfa3f49e8034fabfc232f..ccf2afa7945e15d36f49006bdd16d923cffc6bcd 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -44,6 +44,8 @@ def defaults # rubocop:disable Metrics/AbcSize allow_possible_spam: false, asset_proxy_enabled: false, authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand + autocomplete_users_limit: 300, + autocomplete_users_unauthenticated_limit: 100, ci_max_total_yaml_size_bytes: 314572800, # max_yaml_size_bytes * ci_max_includes = 2.megabyte * 150 commit_email_hostname: default_commit_email_hostname, container_expiration_policies_enable_historic_entries: false, diff --git a/app/validators/json_schemas/application_setting_rate_limits.json b/app/validators/json_schemas/application_setting_rate_limits.json index 4325aeeed5ff4470fe8c818aee6589f073efffb0..f53ab1c23e9d443b721fb76378acf8a34b14fb58 100644 --- a/app/validators/json_schemas/application_setting_rate_limits.json +++ b/app/validators/json_schemas/application_setting_rate_limits.json @@ -4,6 +4,16 @@ "type": "object", "additionalProperties": false, "properties": { + "autocomplete_users_limit": { + "type": "integer", + "minimum": 0, + "description": "Number of authenticated requests allowed to the GET /autocomplete/users endpoint." + }, + "autocomplete_users_unauthenticated_limit": { + "type": "integer", + "minimum": 0, + "description": "Number of unauthenticated requests allowed to the GET /autocomplete/users endpoint." + }, "concurrent_bitbucket_import_jobs_limit": { "type": "integer", "minimum": 1, diff --git a/config/feature_flags/gitlab_com_derisk/autocomplete_users_rate_limit.yml b/config/feature_flags/gitlab_com_derisk/autocomplete_users_rate_limit.yml new file mode 100644 index 0000000000000000000000000000000000000000..a9103b9b110f3c330a5552342973e0a4ad4e403d --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/autocomplete_users_rate_limit.yml @@ -0,0 +1,9 @@ +--- +name: autocomplete_users_rate_limit +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368926 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183244 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/523595 +milestone: '17.10' +group: group::authentication +type: gitlab_com_derisk +default_enabled: false diff --git a/lib/api/settings.rb b/lib/api/settings.rb index a5254b999c02155a790ca6cd4411cc8f72de1154..2b746d3c549b90dc0bafd0a5f632c4c0264de80d 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -219,6 +219,8 @@ def filter_attributes_using_license(attrs) optional :concurrent_relation_batch_export_limit, type: Integer, desc: 'Maximum number of simultaneous batch export jobs to process.' optional :bulk_import_enabled, type: Boolean, desc: 'Enable migrating GitLab groups and projects by direct transfer' optional :bulk_import_max_download_file, type: Integer, desc: 'Maximum download file size in MB when importing from source GitLab instances by direct transfer' + optional :autocomplete_users_limit, type: Integer, desc: 'Rate limit for authenticated requests to users autocomplete endpoint' + optional :autocomplete_users_unauthenticated_limit, type: Integer, desc: 'Rate limit for authenticated requests to users autocomplete endpoint' optional :concurrent_github_import_jobs_limit, type: Integer, desc: 'Github Importer maximum number of simultaneous import jobs' optional :concurrent_bitbucket_import_jobs_limit, type: Integer, desc: 'Bitbucket Cloud Importer maximum number of simultaneous import jobs' optional :concurrent_bitbucket_server_import_jobs_limit, type: Integer, desc: 'Bitbucket Server Importer maximum number of simultaneous import jobs' diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index 24521414d8e86a9afaa3d56ac6a164f54ce524c1..a849f477097248777be4a826bcdf3bf28dd68d9d 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -19,6 +19,8 @@ class << self # and only do that when it's needed. def rate_limits # rubocop:disable Metrics/AbcSize { + autocomplete_users: { threshold: -> { application_settings.autocomplete_users_limit }, interval: 1.minute }, + autocomplete_users_unauthenticated: { threshold: -> { application_settings.autocomplete_users_unauthenticated_limit }, interval: 1.minute }, issues_create: { threshold: -> { application_settings.issues_create_limit }, interval: 1.minute }, notes_create: { threshold: -> { application_settings.notes_create_limit }, interval: 1.minute }, project_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute }, diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 257cb6adc007568ad13090a3bb86d2c57f4eebbb..54d4b1c3ad7017bc8a7a6a757609c132ae557451 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -238,7 +238,25 @@ end end - it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + context 'when autocomplete users feature flag is disabled' do + before do + stub_feature_flags(autocomplete_users_rate_limit: false) + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + let(:current_user) { user } + + def request + get(:users, params: { search: 'foo@bar.com' }) + end + + before do + sign_in(current_user) + end + end + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :autocomplete_users do let(:current_user) { user } def request diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index be7d08e9273e484e451ce89a0d7b6a1e8bccacd9..4d90c8de03979d82df34951bf5820e1aed8971dc 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -38,6 +38,8 @@ it { expect(setting.concurrent_bitbucket_server_import_jobs_limit).to eq(100) } it { expect(setting.nuget_skip_metadata_url_validation).to be(false) } it { expect(setting.silent_admin_exports_enabled).to be(false) } + it { expect(setting.autocomplete_users_limit).to eq(300) } + it { expect(setting.autocomplete_users_unauthenticated_limit).to eq(100) } it { expect(setting.group_api_limit).to eq(400) } it { expect(setting.group_invited_groups_api_limit).to eq(60) } it { expect(setting.group_projects_api_limit).to eq(600) } @@ -337,6 +339,8 @@ def many_usernames(num = 100) where(:attribute) do %i[ helm_max_packages_count + autocomplete_users_limit + autocomplete_users_unauthenticated_limit bulk_import_concurrent_pipeline_batch_limit code_suggestions_api_rate_limit concurrent_bitbucket_import_jobs_limit diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index c5d9e729032b46bd978db931aa2bf1d78b0c2fb4..7d2559603d0836ccfdf0433806d760f6e5cc4238 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -1161,6 +1161,22 @@ end end + context 'with rate limit settings' do + context 'with users autocomplete rate limits' do + it 'updates the settings' do + put( + api("/application/settings", admin), + params: { autocomplete_users_limit: 4242, + autocomplete_users_unauthenticated_limit: 42 } + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['autocomplete_users_limit']).to eq(4242) + expect(json_response['autocomplete_users_unauthenticated_limit']).to eq(42) + end + end + end + context 'security txt settings' do let(:content) { "Contact: foo@acme.com" }