diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1affdd8f4339b6bf6ea4eee597885e8cb0e78a61..2af212fd75c80b0d1451971907a99796d06e993a 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -514,7 +514,8 @@ def visible_attributes :ci_max_total_yaml_size_bytes, :project_jobs_api_rate_limit, :security_txt_content, - :allow_project_creation_for_guest_and_below + :allow_project_creation_for_guest_and_below, + :downstream_pipeline_trigger_limit_per_project_user_sha ].tap do |settings| next if Gitlab.com? diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 4cb918c9c8b0dbec42cf5558022f5862f4e791d5..e39dc127641499a8f6e22ce805e1211280b631af 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -597,11 +597,13 @@ def self.kroki_formats_attributes :sidekiq_job_limiter_compression_threshold_bytes, :sidekiq_job_limiter_limit_bytes, :terminal_max_session_time, - :users_get_by_id_limit + :users_get_by_id_limit, + :downstream_pipeline_trigger_limit_per_project_user_sha end jsonb_accessor :rate_limits, - members_delete_limit: [:integer, { default: 60 }] + members_delete_limit: [:integer, { default: 60 }], + downstream_pipeline_trigger_limit_per_project_user_sha: [:integer, { default: 0 }] validates :rate_limits, json_schema: { filename: "application_setting_rate_limits" } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index e0b334780371e4f12c78eec3b60add17fac50afd..c3ff9c50e764661c36810d1e695d93148b121a70 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -280,7 +280,8 @@ def defaults # rubocop:disable Metrics/AbcSize security_txt_content: nil, allow_project_creation_for_guest_and_below: true, enable_member_promotion_management: false, - security_approval_policies_limit: 5 + security_approval_policies_limit: 5, + downstream_pipeline_trigger_limit_per_project_user_sha: 0 }.tap do |hsh| hsh.merge!(non_production_defaults) unless Rails.env.production? end diff --git a/app/services/ci/trigger_downstream_pipeline_service.rb b/app/services/ci/trigger_downstream_pipeline_service.rb index ac95c4596b1379a7cb82ebf80639ca37d4503cf0..4fc09fd749288250a98972c1813d3aee6d113646 100644 --- a/app/services/ci/trigger_downstream_pipeline_service.rb +++ b/app/services/ci/trigger_downstream_pipeline_service.rb @@ -3,10 +3,6 @@ module Ci # Enqueues the downstream pipeline worker. class TriggerDownstreamPipelineService - # This is a temporary constant. It may be converted into an application setting - # in the future. See https://gitlab.com/gitlab-org/gitlab/-/issues/425941. - DOWNSTREAM_PIPELINE_TRIGGER_LIMIT_PER_PROJECT_USER_SHA = 200 - def initialize(bridge) @bridge = bridge @current_user = bridge.user diff --git a/app/validators/json_schemas/application_setting_rate_limits.json b/app/validators/json_schemas/application_setting_rate_limits.json index e74295291dfdc0eb7f1e21dc75111686096331fa..183c6327447175215883caa1477cebcd9f3f5753 100644 --- a/app/validators/json_schemas/application_setting_rate_limits.json +++ b/app/validators/json_schemas/application_setting_rate_limits.json @@ -8,6 +8,11 @@ "type": "integer", "minimum": 0, "description": "Number of project or group members a user can delete per minute." + }, + "downstream_pipeline_trigger_limit_per_project_user_sha": { + "type": "integer", + "minimum": 0, + "description": "Maximum number of downstream pipelines triggered in a project per user" } } } diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index f63a9862c13c5e05effd4382d1cf58895dace537..a808c580c2fe84c49110feabd51bf9446098fc9d 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -50,6 +50,11 @@ = f.number_field :ci_max_includes, class: 'form-control gl-form-input' .form-text.text-muted = s_('AdminSettings|The maximum number of included files per pipeline.') + .form-group + = f.label :downstream_pipeline_trigger_limit_per_project_user_sha, s_('AdminSettings|Maximum downstream pipelines triggered in a project per user'), class: 'label-bold' + = f.number_field :downstream_pipeline_trigger_limit_per_project_user_sha, min: 0, class: 'form-control gl-form-input' + .form-text.text-muted + = s_('AdminSettings|The maximum number of downstream pipelines triggered in a project per user.') .form-group = f.label :ci_config_path, _('Default CI/CD configuration file'), class: 'label-bold' = f.text_field :default_ci_config_path, class: 'form-control gl-form-input', placeholder: '.gitlab-ci.yml' diff --git a/doc/administration/settings/continuous_integration.md b/doc/administration/settings/continuous_integration.md index 67f5889dd61ecd086a4ad056b536c4d73883a84a..d8ba43d78a29d0411dae2bf97c67049881bd4b24 100644 --- a/doc/administration/settings/continuous_integration.md +++ b/doc/administration/settings/continuous_integration.md @@ -205,6 +205,18 @@ The default is `150`. 1. Change the value of **Maximum includes**. 1. Select **Save changes** for the changes to take effect. +## Maximum downstream pipelines triggered per project + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144077) in GitLab 16.10 [with feature flag](../feature_flags.md) named `ci_rate_limit_downstream_pipelines`. Disabled by default. + +The maximum number of [downstream pipelines](../../ci/pipelines/downstream_pipelines.md) per project per user can be set at the instance level. +The default is `0` (no restriction). + +1. On the left sidebar, at the bottom, select **Admin Area**. +1. Select **Settings > CI/CD**. +1. Change the value of **Maximum downstream pipelines triggered in a project per user**. +1. Select **Save changes** for the changes to take effect. + ## Default CI/CD configuration file > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18073) in GitLab 12.5. diff --git a/doc/api/settings.md b/doc/api/settings.md index 5e2663e9fec071d8466ed4a3c880e78159fb0a4a..922cb2bd3c86d679ae175985665e91635ef15758 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -281,7 +281,8 @@ Example response: "bulk_import_max_download_file_size": 5120, "project_jobs_api_rate_limit": 600, "security_txt_content": null, - "bulk_import_concurrent_pipeline_batch_limit": 25 + "bulk_import_concurrent_pipeline_batch_limit": 25, + "downstream_pipeline_trigger_limit_per_project_user_sha": 0 } ``` @@ -390,6 +391,7 @@ listed in the descriptions of the relevant settings. | `domain_denylist_enabled` | boolean | no | (**If enabled, requires:** `domain_denylist`) Allows blocking sign-ups from emails from specific domains. | | `domain_denylist` | array of strings | no | Users with email addresses that match these domains **cannot** sign up. Wildcards allowed. Use separate lines for multiple entries. For example: `domain.com`, `*.domain.com`. | | `domain_allowlist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. | +| `downstream_pipeline_trigger_limit_per_project_user_sha` | integer | no | Rate limit creation of downstream pipelines. Default: `0`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144077) in GitLab 16.10 with a [flag](../administration/feature_flags.md) named `ci_rate_limit_downstream_pipelines`. Disabled by default. | | `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | | `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. | | `ecdsa_sk_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA_SK key. Default is `0` (no restriction). `-1` disables ECDSA_SK keys. | diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 6ef68ccc3de6383cd160a0eab4f91f30a93c08b3..8c542db06a74c1497850f64eee20f0ecad3190a8 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -229,6 +229,7 @@ def filter_attributes_using_license(attrs) optional :namespace_aggregation_schedule_lease_duration_in_seconds, type: Integer, desc: 'Maximum duration (in seconds) between refreshes of namespace statistics (Default: 300)' optional :project_jobs_api_rate_limit, type: Integer, desc: 'Maximum authenticated requests to /project/:id/jobs per minute' optional :security_txt_content, type: String, desc: 'Public security contact information made available at https://gitlab.example.com/.well-known/security.txt' + optional :downstream_pipeline_trigger_limit_per_project_user_sha, type: Integer, desc: 'Maximum number of downstream pipelines triggered per project' Gitlab::SSHPublicKey.supported_types.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index 2e992e38a44c70f9896174f2415f400ee6215f31..e13b54e40eea40bfaaae9062a6156d52cd7feb41 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -67,7 +67,7 @@ def rate_limits # rubocop:disable Metrics/AbcSize threshold: -> { application_settings.projects_api_rate_limit_unauthenticated }, interval: 10.minutes }, downstream_pipeline_trigger: { - threshold: -> { ::Ci::TriggerDownstreamPipelineService::DOWNSTREAM_PIPELINE_TRIGGER_LIMIT_PER_PROJECT_USER_SHA }, interval: 1.minute + threshold: -> { application_settings.downstream_pipeline_trigger_limit_per_project_user_sha }, interval: 1.minute } }.freeze end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 65bb8ec785da09ba5b52e9127a4a637c13146203..5b49c71adee3c3a6066901a2cca67d76b2c870b3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3584,6 +3584,9 @@ msgstr "" msgid "AdminSettings|Limit the number of namespaces and projects that can be indexed." msgstr "" +msgid "AdminSettings|Maximum downstream pipelines triggered in a project per user" +msgstr "" + msgid "AdminSettings|Maximum duration of a session for Git operations when 2FA is enabled." msgstr "" @@ -3743,6 +3746,9 @@ msgstr "" msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire." msgstr "" +msgid "AdminSettings|The maximum number of downstream pipelines triggered in a project per user." +msgstr "" + msgid "AdminSettings|The maximum number of included files per pipeline." msgstr "" diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index d1fdbfc5329f33e7788d5a04bc9d0bd334b25911..8db9c45dbef90f98b06fc9bfb349564e4b874018 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -459,6 +459,7 @@ uncheck 'Keep the latest artifacts for all jobs in the latest successful pipelines' uncheck 'Enable pipeline suggestion banner' fill_in 'application_setting_ci_max_includes', with: 200 + fill_in 'application_setting_downstream_pipeline_trigger_limit_per_project_user_sha', with: 500 click_button 'Save changes' end @@ -467,6 +468,7 @@ expect(current_settings.keep_latest_artifact).to be false expect(current_settings.suggest_pipeline_enabled).to be false expect(current_settings.ci_max_includes).to be 200 + expect(current_settings.downstream_pipeline_trigger_limit_per_project_user_sha).to be 500 expect(page).to have_content "Application settings saved successfully" end diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index b378437c407555a70216ecb84af2570a4c2fd74f..43075a75d1ca1f025626be5bb0fa958f6fb52735 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -65,7 +65,7 @@ project_download_export_limit project_export_limit project_import_limit raw_blob_request_limit group_export_limit group_download_export_limit group_import_limit users_get_by_id_limit search_rate_limit search_rate_limit_unauthenticated - members_delete_limit + members_delete_limit downstream_pipeline_trigger_limit_per_project_user_sha ]) end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 18060d29ad52c74a383d5a7c13e8fdd6cb9f4c61..d88415527ad420f5634d95a5d8b0856bea419416 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -29,6 +29,7 @@ it { expect(setting.bulk_import_concurrent_pipeline_batch_limit).to eq(25) } it { expect(setting.allow_project_creation_for_guest_and_below).to eq(true) } it { expect(setting.members_delete_limit).to eq(60) } + it { expect(setting.downstream_pipeline_trigger_limit_per_project_user_sha).to eq(0) } end describe 'validations' do @@ -244,6 +245,7 @@ def many_usernames(num = 100) sidekiq_job_limiter_limit_bytes terminal_max_session_time users_get_by_id_limit + downstream_pipeline_trigger_limit_per_project_user_sha ] end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 01aeb88a1096859b4ac2119149d6ff2ceeafbc5e..018e6b9786418ba01d91b9c8c76a107322337ddc 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -95,6 +95,7 @@ expect(json_response['max_login_attempts']).to be_nil expect(json_response['failed_login_attempts_unlock_period_in_minutes']).to be_nil expect(json_response['bulk_import_concurrent_pipeline_batch_limit']).to eq(25) + expect(json_response['downstream_pipeline_trigger_limit_per_project_user_sha']).to eq(0) end end @@ -216,7 +217,8 @@ gitlab_shell_operation_limit: 500, namespace_aggregation_schedule_lease_duration_in_seconds: 400, max_import_remote_file_size: 2, - security_txt_content: nil + security_txt_content: nil, + downstream_pipeline_trigger_limit_per_project_user_sha: 300 } expect(response).to have_gitlab_http_status(:ok) @@ -302,6 +304,7 @@ expect(json_response['bulk_import_max_download_file_size']).to be(1) expect(json_response['security_txt_content']).to be(nil) expect(json_response['bulk_import_concurrent_pipeline_batch_limit']).to be(2) + expect(json_response['downstream_pipeline_trigger_limit_per_project_user_sha']).to be(300) end end @@ -1023,6 +1026,40 @@ end end + context 'with downstream_pipeline_trigger_limit_per_project_user_sha' do + it 'updates the settings' do + put api("/application/settings", admin), params: { + downstream_pipeline_trigger_limit_per_project_user_sha: 200 + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + 'downstream_pipeline_trigger_limit_per_project_user_sha' => 200 + ) + end + + it 'allows a zero value' do + put api("/application/settings", admin), params: { + downstream_pipeline_trigger_limit_per_project_user_sha: 0 + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + 'downstream_pipeline_trigger_limit_per_project_user_sha' => 0 + ) + end + + it 'does not allow a nil value' do + put api("/application/settings", admin), params: { + downstream_pipeline_trigger_limit_per_project_user_sha: nil + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['downstream_pipeline_trigger_limit_per_project_user_sha']) + .to include(a_string_matching('is not a number')) + end + end + context 'with housekeeping enabled' do it 'at least one of housekeeping_incremental_repack_period or housekeeping_optimize_repository_period is required' do put api("/application/settings", admin), params: { diff --git a/spec/services/ci/trigger_downstream_pipeline_service_spec.rb b/spec/services/ci/trigger_downstream_pipeline_service_spec.rb index 71d6931658925faa279856864c9bdf5c9edb03d9..299f765b95cd8cb69254e0386cf98fb8254a33c7 100644 --- a/spec/services/ci/trigger_downstream_pipeline_service_spec.rb +++ b/spec/services/ci/trigger_downstream_pipeline_service_spec.rb @@ -50,7 +50,7 @@ context 'when the limit is exceeded' do before do - stub_const("#{described_class.name}::DOWNSTREAM_PIPELINE_TRIGGER_LIMIT_PER_PROJECT_USER_SHA", 1) + stub_application_setting(downstream_pipeline_trigger_limit_per_project_user_sha: 1) end it 'drops the bridge and does not schedule the downstream pipeline worker', :aggregate_failures do