diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index d3717509c91219d6989b65d364b937bdf04ef126..8933caaa234fc944fd24c6faaec7a18741ce5017 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -555,6 +555,13 @@ def visible_attributes :user_contributed_projects_api_limit, :user_projects_api_limit, :user_starred_projects_api_limit, + :users_api_limit_followers, + :users_api_limit_following, + :users_api_limit_status, + :users_api_limit_ssh_keys, + :users_api_limit_ssh_key, + :users_api_limit_gpg_keys, + :users_api_limit_gpg_key, :gitlab_dedicated_instance, :gitlab_environment_toolkit_instance, :ci_max_includes, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3edcbc91a3d11a9b45cc47792733faef02cc02ba..7fe08e84f08177ad16e265e3c5052713abf83cf9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -668,7 +668,14 @@ def self.kroki_formats_attributes :user_contributed_projects_api_limit, :user_projects_api_limit, :user_starred_projects_api_limit, - :users_get_by_id_limit + :users_get_by_id_limit, + :users_api_limit_followers, + :users_api_limit_following, + :users_api_limit_status, + :users_api_limit_ssh_keys, + :users_api_limit_ssh_key, + :users_api_limit_gpg_keys, + :users_api_limit_gpg_key end attribute :resource_usage_limits, ::Gitlab::Database::Type::IndifferentJsonb.new, default: -> { {} } @@ -692,7 +699,14 @@ def self.kroki_formats_attributes projects_api_limit: [:integer, { default: 2000 }], user_contributed_projects_api_limit: [:integer, { default: 100 }], user_projects_api_limit: [:integer, { default: 300 }], - user_starred_projects_api_limit: [:integer, { default: 100 }] + user_starred_projects_api_limit: [:integer, { default: 100 }], + users_api_limit_followers: [:integer, { default: 100 }], + users_api_limit_following: [:integer, { default: 100 }], + users_api_limit_status: [:integer, { default: 240 }], + users_api_limit_ssh_keys: [:integer, { default: 120 }], + users_api_limit_ssh_key: [:integer, { default: 120 }], + users_api_limit_gpg_keys: [:integer, { default: 120 }], + users_api_limit_gpg_key: [:integer, { default: 120 }] jsonb_accessor :service_ping_settings, gitlab_environment_toolkit_instance: [:boolean, { default: false }] diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 4c838cffa211db3f06684b7a9d991eae068a20e5..374f590e84186885ca408edd21c3a0af0ae704b2 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -299,6 +299,13 @@ def defaults # rubocop:disable Metrics/AbcSize user_contributed_projects_api_limit: 100, user_projects_api_limit: 300, user_starred_projects_api_limit: 100, + users_api_limit_followers: 100, + users_api_limit_following: 100, + users_api_limit_status: 240, + users_api_limit_ssh_keys: 120, + users_api_limit_ssh_key: 120, + users_api_limit_gpg_keys: 120, + users_api_limit_gpg_key: 120, nuget_skip_metadata_url_validation: false, ai_action_api_rate_limit: 160, code_suggestions_api_rate_limit: 60, diff --git a/app/validators/json_schemas/application_setting_rate_limits.json b/app/validators/json_schemas/application_setting_rate_limits.json index e97eee3185df3dd1c2eaa93e5b0bea949c287f7e..4325aeeed5ff4470fe8c818aee6589f073efffb0 100644 --- a/app/validators/json_schemas/application_setting_rate_limits.json +++ b/app/validators/json_schemas/application_setting_rate_limits.json @@ -93,6 +93,41 @@ "type": "integer", "minimum": 0, "description": "Maximum number of downstream pipelines that can be triggered per minute (for a given project, user, and commit)." + }, + "users_api_limit_followers": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:id/followers API." + }, + "users_api_limit_following": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:id/following API." + }, + "users_api_limit_status": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:user_id/status API." + }, + "users_api_limit_ssh_keys": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:user_id/keys API." + }, + "users_api_limit_ssh_key": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:id/keys/:key_id API." + }, + "users_api_limit_gpg_keys": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:id/gpg_keys API." + }, + "users_api_limit_gpg_key": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the GET /users/:id/gpg_keys/:key_id API." } } } diff --git a/app/views/admin/application_settings/_users_api_limits.html.haml b/app/views/admin/application_settings/_users_api_limits.html.haml index ddeb4bd77102bf89d6f9034c104f4f56979c77ed..5f6dea377bb1cc42858c44220cfc7605e3c686a2 100644 --- a/app/views/admin/application_settings/_users_api_limits.html.haml +++ b/app/views/admin/application_settings/_users_api_limits.html.haml @@ -1,14 +1,48 @@ -= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-users-api-limits-settings'), html: { class: 'fieldset-form' } do |f| - = form_errors(@application_setting) - - %fieldset - .form-group - = f.label :users_get_by_id_limit, _('Maximum requests per 10 minutes per user'), class: 'label-bold' - = f.number_field :users_get_by_id_limit, class: 'form-control gl-form-input' - .form-group - = f.label :users_get_by_id_limit_allowlist_raw, _('Users to exclude from the rate limit'), class: 'label-bold' - = f.text_area :users_get_by_id_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'users-api-limit-users-allowlist-field-description' } - .form-text.gl-text-subtle{ id: 'users-api-limit-users-allowlist-field-description' } - = _('List of users who are allowed to exceed the rate limit. Example: username1, username2') - - = f.submit _('Save changes'), pajamas_button: true += render ::Layouts::SettingsBlockComponent.new(_('Users API rate limits'), + id: 'js-users-api-limits-settings', + testid: 'users-api-limits-settings', + expanded: expanded_by_default?) do |c| + - c.with_description do + = _('Set the per-user and per-IP address rate limits for the requests to Users API.') + = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_users_api.md'), target: '_blank', rel: 'noopener noreferrer' + - c.with_body do + = gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-users-api-limits-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + = _('Set to 0 to disable rate limits.') + + %fieldset + .form-group + = f.label :users_api_limit_followers, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/followers', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_followers, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_api_limit_following, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/following', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_following, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_api_limit_status, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:user_id/status', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_status, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_api_limit_ssh_keys, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:user_id/keys', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_ssh_keys, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_api_limit_ssh_key, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/keys/:key_id', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_ssh_key, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_api_limit_gpg_keys, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/gpg_keys', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_gpg_keys, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_api_limit_gpg_key, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/gpg_keys/:key_id', timeframe: 'minute'), class: 'label-bold' + = f.number_field :users_api_limit_gpg_key, min: 0, class: 'form-control gl-form-input' + + %fieldset + .form-group + = f.label :users_get_by_id_limit, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user'), api_name: 'GET /users/:id', timeframe: '10 minutes'), class: 'label-bold' + = f.number_field :users_get_by_id_limit, min: 0, class: 'form-control gl-form-input' + .form-group + = f.label :users_get_by_id_limit_allowlist_raw, _('Excluded users'), class: 'label-bold' + = f.text_area :users_get_by_id_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'users-api-limit-users-allowlist-field-description' } + .form-text.gl-text-subtle{ id: 'users-api-limit-users-allowlist-field-description' } + = _('List of users allowed to exceed the rate limit. Example: username1, username2') + + = f.submit _('Save changes'), pajamas_button: true diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml index 30d4feff27274fc62be4ce650ce6e836ae4e380d..81b9d7c8093f29296516beb41e0f4d7bad36fc19 100644 --- a/app/views/admin/application_settings/network.html.haml +++ b/app/views/admin/application_settings/network.html.haml @@ -119,15 +119,7 @@ - c.with_body do = render 'note_limits' -= render ::Layouts::SettingsBlockComponent.new(_('Users API rate limits'), - id: 'js-users-api-limits-settings', - testid: 'users-api-limits-settings', - expanded: expanded_by_default?) do |c| - - c.with_description do - = _('Set the per-user rate limit for getting a user by ID via the API.') - = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_users_api.md'), target: '_blank', rel: 'noopener noreferrer' - - c.with_body do - = render 'users_api_limits' += render 'users_api_limits' = render 'groups_api_limits' diff --git a/doc/administration/settings/rate_limit_on_users_api.md b/doc/administration/settings/rate_limit_on_users_api.md index 306004aa4342509665c5c4a17fce3c2aa83254e1..90246ab7aebd4cdf5a0a5a4d27e11d7ef197f9d2 100644 --- a/doc/administration/settings/rate_limit_on_users_api.md +++ b/doc/administration/settings/rate_limit_on_users_api.md @@ -12,25 +12,46 @@ title: Rate limits on Users API {{< /details >}} -You can configure the per user rate limit for requests to [Users API](../../api/users.md). +> - Rate limits for Users API [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/452349) in GitLab 17.1 with a [flag](../feature_flags.md) named `rate_limiting_user_endpoints`. Disabled by default. +> - [Added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) customizable rate limits in GitLab 17.10. + +You can configure the per minute rate limit per IP address and per user for requests to the following [Users API](../../api/users.md). + +FLAG: +The availability of this feature is controlled by a feature flag. +For more information, see the history. +This feature is available for testing, but not ready for production use. + +| Limit | Default | +|-----------------------------------------------------------------|---------| +| [GET /users/:id/followers](../../api/user_follow_unfollow.md#list-all-accounts-that-follow-a-user) | 100 | +| [GET /users/:id/following](../../api/user_follow_unfollow.md#list-all-accounts-followed-by-a-user) | 100 | +| [GET /users/:id/status](../../api/users.md#get-the-status-of-a-user) | 240 | +| [GET /users/:id/keys](../../api/user_keys.md#list-all-ssh-keys-for-a-user) | 120 | +| [GET /users/:id/gpg_keys](../../api/user_keys.md#list-all-gpg-keys-for-a-user) | 120 | +| [GET /users/:id/gpg_keys/:key_id](../../api/user_keys.md#get-a-gpg-key-for-a-user) | 120 | To change the rate limit: 1. On the left sidebar, at the bottom, select **Admin**. 1. Select **Settings > Network**. 1. Expand **Users API rate limit**. -1. In the **Maximum requests per 10 minutes** text box, enter the new value. -1. Optional. In the **Users to exclude from the rate limit** box, list users allowed to exceed the limit. +1. Set values for any available rate limit. The rate limits are per minute, per user for authenticated requests and per IP address for unauthenticated requests. Enter `0` to disable a rate limit. 1. Select **Save changes**. -This limit is: +Each rate limit: + +- Applies per user if the request is authenticated. +- Applies per IP address if the request is unauthenticated. +- Can be set to `0` to disable rate limits. -- Applied independently per user. -- Not applied per IP address. +Logs: -The default value is `300`. +- Requests that exceed the rate limit are logged to the `auth.log` file. +- Rate limit modifications are logged to the `audit_json.log` file. -Requests over the rate limit are logged into the `auth.log` file. +Example: -For example, if you set a limit of 300, requests to the `GET /users/:id` API endpoint -exceeding a rate of 300 per 10 minutes are blocked. Access to the endpoint is allowed after ten minutes have elapsed. +If you set a rate limit of 150 for `GET /users/:id/followers` and send 155 requests in a minute, the +final five requests are blocked. After a minute, you could continue sending requests until you +exceed the rate limit again. diff --git a/doc/api/settings.md b/doc/api/settings.md index 6b99a35bb91eb8c862ed43a126bca9f28e914c48..8707ced48db02640a37af007974e7ad7a1f625a3 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -328,6 +328,13 @@ Example response: "jira_connect_proxy_url": "http://gitlab.example.com", "user_defaults_to_private_profile": true, "projects_api_rate_limit_unauthenticated": 400, + "users_api_limit_followers": 100, + "users_api_limit_following": 100, + "users_api_limit_status": 240, + "users_api_limit_ssh_keys": 120, + "users_api_limit_ssh_key": 120, + "users_api_limit_gpg_keys": 120, + "users_api_limit_gpg_key": 120, "silent_mode_enabled": false, "security_policy_global_group_approvers_enabled": true, "security_approval_policies_limit": 5, @@ -620,6 +627,13 @@ to configure other related settings. These requirements are | `project_export_enabled` | boolean | no | Enable project export. | | `project_jobs_api_rate_limit` | integer | no | Maximum authenticated requests to `/project/:id/jobs` per minute. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129319) in GitLab 16.5. Default: 600. | | `projects_api_rate_limit_unauthenticated` | integer | no | [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10. Max number of requests per 10 minutes per IP address for unauthenticated requests to the [list all projects API](projects.md#list-all-projects). Default: 400. To disable throttling set to 0.| +| `users_api_limit_following` | integer | no | Max number of requests per minute, per user or IP address. Default: 100. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | +| `users_api_limit_followers` | integer | no | Max number of requests per minute, per user or IP address. Default: 100. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | +| `users_api_limit_status` | integer | no | Max number of requests per minute, per user or IP address. Default: 240. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | +| `users_api_limit_keys` | integer | no | Max number of requests per minute, per user or IP address. Default: 120. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | +| `users_api_limit_key` | integer | no | Max number of requests per minute, per user or IP address. Default: 120. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | +| `users_api_limit_gpg_keys` | integer | no | Max number of requests per minute, per user or IP address. Default: 120. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | +| `users_api_limit_gpg_key` | integer | no | Max number of requests per minute, per user or IP address. Default: 120. Set to `0` to disable limits. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181054) in GitLab 17.10. | | `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. | | `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. | | `disable_overriding_approvers_per_merge_request` | boolean | no | Prevent editing approval rules in projects and merge requests | diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md index 3d8415e565fafaafbf757e722c583738e01a1f7a..3498961195d9a884c40bd807063075bd95ecd585 100644 --- a/doc/security/rate_limits.md +++ b/doc/security/rate_limits.md @@ -61,6 +61,7 @@ You can set these rate limits in the **Admin** area of your instance: - [Incident management rate limits](../administration/settings/incident_management_rate_limits.md) - [Projects API rate limits](../administration/settings/rate_limit_on_projects_api.md) - [Groups API rate limits](../administration/settings/rate_limit_on_groups_api.md) +- [Users API rate limits](../administration/settings/rate_limit_on_users_api.md) - [Organizations API rate limits](../administration/settings/rate_limit_on_organizations_api.md) You can set these rate limits using the Rails console: @@ -96,20 +97,6 @@ For configuration information, see ## Non-configurable limits -{{< history >}} - -- Rate limit on the `:user_id/status`, `:id/following`, `:id/followers`, `:user_id/keys`, `id/keys/:key_id`, `:id/gpg_keys`, and `:id/gpg_keys/:key_id` endpoints [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/452349) in GitLab 17.1 [with a flag](../administration/feature_flags.md) named `rate_limiting_user_endpoints`. Disabled by default. - -{{< /history >}} - -{{< alert type="flag" >}} - -The availability of multiple endpoints in this feature is controlled by a feature flag. -For more information, see the history. -These endpoints are available for testing, but not ready for production use. - -{{< /alert >}} - ### Repository archives A rate limit for [downloading repository archives](../api/repositories.md#get-file-archive) is @@ -131,48 +118,6 @@ discover usernames or email addresses in use. The **rate limit** is 20 calls per minute per IP address. -### User status - -There is a rate limit per IP address on the `:user_id/status` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 240 calls per minute per IP address. - -### User following - -There is a rate limit per IP address on the `:id/following` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 100 calls per minute per IP address. - -### User followers - -There is a rate limit per IP address on the `:id/followers` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 100 calls per minute per IP address. - -### User keys - -There is a rate limit per IP address on the `:user_id/keys` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 120 calls per minute per IP address. - -### User specific key - -There is a rate limit per IP address on the `id/keys/:key_id` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 120 calls per minute per IP address. - -### User GPG keys - -There is a rate limit per IP address on the `:id/gpg_keys` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 120 calls per minute per IP address. - -### User specific GPG keys - -There is a rate limit per IP address on the `:id/gpg_keys/:key_id` endpoint. This is to mitigate attempts to misuse the endpoint. - -The **rate limit** is 120 calls per minute per IP address. - ### Update username There is a rate limit on how frequently a username can be changed. This is enforced to mitigate misuse of the feature. For example, to mass discover diff --git a/lib/api/users.rb b/lib/api/users.rb index 11bdd067b0a0ff94a1fe989616d7a695d8b3287a..75bb425cf928a76e18b61fef5c5feaf20b0018f0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -533,7 +533,7 @@ def reorder_users(users) end get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :system_access do if Feature.enabled?(:rate_limiting_user_endpoints, ::Feature.current_request) - check_rate_limit_by_user_or_ip!(:user_keys) + check_rate_limit_by_user_or_ip!(:user_ssh_keys) end user = find_user(params[:user_id]) @@ -552,7 +552,7 @@ def reorder_users(users) end get ':id/keys/:key_id', requirements: API::USER_REQUIREMENTS, feature_category: :system_access do if Feature.enabled?(:rate_limiting_user_endpoints, ::Feature.current_request) - check_rate_limit_by_user_or_ip!(:user_specific_key) + check_rate_limit_by_user_or_ip!(:user_ssh_key) end user = find_user(params[:id]) @@ -645,7 +645,7 @@ def reorder_users(users) # rubocop: disable CodeReuse/ActiveRecord get ':id/gpg_keys/:key_id', feature_category: :system_access do if Feature.enabled?(:rate_limiting_user_endpoints, ::Feature.current_request) - check_rate_limit_by_user_or_ip!(:user_specific_gpg_key) + check_rate_limit_by_user_or_ip!(:user_gpg_key) end user = User.find_by(id: params[:id]) diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index bad6ba9032bc0fec42569ac3a83158671f0eb0fc..a66255dbac62cff48c46e8eced1bdb8d12d979a5 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -53,13 +53,13 @@ def rate_limits # rubocop:disable Metrics/AbcSize web_hook_event_resend: { threshold: 5, interval: 1.minute }, users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes }, username_exists: { threshold: 20, interval: 1.minute }, - user_followers: { threshold: 100, interval: 1.minute }, - user_following: { threshold: 100, interval: 1.minute }, - user_status: { threshold: 240, interval: 1.minute }, - user_keys: { threshold: 120, interval: 1.minute }, - user_specific_key: { threshold: 120, interval: 1.minute }, - user_gpg_keys: { threshold: 120, interval: 1.minute }, - user_specific_gpg_key: { threshold: 120, interval: 1.minute }, + user_followers: { threshold: -> { application_settings.users_api_limit_followers }, interval: 1.minute }, + user_following: { threshold: -> { application_settings.users_api_limit_following }, interval: 1.minute }, + user_status: { threshold: -> { application_settings.users_api_limit_status }, interval: 1.minute }, + user_ssh_keys: { threshold: -> { application_settings.users_api_limit_ssh_keys }, interval: 1.minute }, + user_ssh_key: { threshold: -> { application_settings.users_api_limit_ssh_key }, interval: 1.minute }, + user_gpg_keys: { threshold: -> { application_settings.users_api_limit_gpg_keys }, interval: 1.minute }, + user_gpg_key: { threshold: -> { application_settings.users_api_limit_gpg_key }, interval: 1.minute }, user_sign_up: { threshold: 20, interval: 1.minute }, user_sign_in: { threshold: 5, interval: 10.minutes }, profile_resend_email_confirmation: { threshold: 5, interval: 1.minute }, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index abd32541cc56562c8ff36733501dcd7280d9e204..c3d01817fa5d98f4ef6ee85deece54572da0756d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23727,6 +23727,9 @@ msgstr "" msgid "Exceptions" msgstr "" +msgid "Excluded users" +msgstr "" + msgid "Excluding %d project with no DORA metrics." msgid_plural "Excluding %d projects with no DORA metrics." msgstr[0] "" @@ -34046,6 +34049,9 @@ msgstr "" msgid "List of suitable GCP locations" msgstr "" +msgid "List of users allowed to exceed the rate limit. Example: username1, username2" +msgstr "" + msgid "List of users who are allowed to exceed the rate limit. Example: username1, username2" msgstr "" @@ -34841,9 +34847,6 @@ msgstr "" msgid "Maximum push size (MiB)" msgstr "" -msgid "Maximum requests per 10 minutes per user" -msgstr "" - msgid "Maximum requests per minute" msgstr "" @@ -54062,7 +54065,7 @@ msgstr "" msgid "Set the per-user and per-IP address rate limits for the requests to Projects API." msgstr "" -msgid "Set the per-user rate limit for getting a user by ID via the API." +msgid "Set the per-user and per-IP address rate limits for the requests to Users API." msgstr "" msgid "Set the per-user rate limit for notes created by web or API requests." @@ -54089,6 +54092,9 @@ msgstr "" msgid "Set to 0 for no size limit." msgstr "" +msgid "Set to 0 to disable rate limits." +msgstr "" + msgid "Set to 0 to disable the limit." msgstr "" diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index cadb6c1a9ce0844cb4147a5ea15431a9e3a8f691..c8a8b8a652e2ab4062e75e0292044db5512ef0d3 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -855,20 +855,6 @@ expect(current_settings.pipeline_limit_per_project_user_sha).to eq(10) end - it 'changes Users API rate limits settings' do - visit network_admin_application_settings_path - - within_testid('users-api-limits-settings') do - fill_in 'Maximum requests per 10 minutes per user', with: 0 - fill_in 'Users to exclude from the rate limit', with: 'someone, someone_else' - click_button 'Save changes' - end - - expect(page).to have_content 'Application settings saved successfully' - expect(current_settings.users_get_by_id_limit).to eq(0) - expect(current_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else]) - end - it 'changes gitlab shell operation limits settings' do visit network_admin_application_settings_path @@ -896,6 +882,103 @@ end end + describe 'users API rate limits' do + let_it_be(:network_settings_section) { 'users-api-limits-settings' } + + context 'for GET /users:id API requests', :aggregate_failures do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user'), api_name: 'GET /users/:id', timeframe: '10 minutes') + end + + let(:application_setting_key) { :users_get_by_id_limit } + + it 'changes Users API rate limits settings', :aggregate_failures do + visit network_admin_application_settings_path + + new_rate_limit = 0 + within_testid('users-api-limits-settings') do + fill_in rate_limit_field, with: new_rate_limit + fill_in 'Excluded users', with: 'someone, someone_else' + click_button 'Save changes' + end + + expect(page).to have_content 'Application settings saved successfully' + expect(current_settings[application_setting_key]).to eq(new_rate_limit) + expect(current_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else]) + end + end + + context 'for GET /users/:id/followers API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/followers', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_followers } + + it_behaves_like 'API rate limit setting' + end + + context 'for GET /users/:id/following API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/following', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_following } + + it_behaves_like 'API rate limit setting' + end + + context 'for GET /users/:user_id/status API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:user_id/status', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_status } + + it_behaves_like 'API rate limit setting' + end + + context 'for GET /users/:user_id/keys API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:user_id/keys', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_ssh_keys } + + it_behaves_like 'API rate limit setting' + end + + context 'for GET /users/:id/keys/:key_id API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/keys/:key_id', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_ssh_key } + + it_behaves_like 'API rate limit setting' + end + + context 'for GET /users/:id/gpg_keys API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/gpg_keys', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_gpg_keys } + + it_behaves_like 'API rate limit setting' + end + + context 'for GET /users/:id/gpg_keys/:key_id API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:id/gpg_keys/:key_id', timeframe: 'minute') + end + + let(:application_setting_key) { :users_api_limit_gpg_key } + + it_behaves_like 'API rate limit setting' + end + end + describe 'organizations API rate limits' do let_it_be(:network_settings_section) { 'organizations-api-limits-settings' } diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index b78d019a0bda75954258f05836f2cc50eef0667d..af71fe131fc98c89eb1487d9a63f34c9e7075dd3 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ApplicationSettingsHelper do +RSpec.describe ApplicationSettingsHelper, feature_category: :shared do include Devise::Test::ControllerHelpers let_it_be(:current_user) { build_stubbed(:admin) } @@ -77,6 +77,8 @@ members_delete_limit downstream_pipeline_trigger_limit_per_project_user_sha group_api_limit group_projects_api_limit groups_api_limit project_api_limit projects_api_limit user_contributed_projects_api_limit user_projects_api_limit user_starred_projects_api_limit + users_api_limit_followers users_api_limit_following users_api_limit_status users_api_limit_ssh_keys + users_api_limit_ssh_key users_api_limit_gpg_keys users_api_limit_gpg_key group_shared_groups_api_limit group_invited_groups_api_limit project_invited_groups_api_limit diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 52c2780657acc6c1de9b29ebdba2f5bb21612706..81820d1cec02201f0fac878ef0e140d0638ed532 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -49,6 +49,13 @@ it { expect(setting.user_contributed_projects_api_limit).to eq(100) } it { expect(setting.user_projects_api_limit).to eq(300) } it { expect(setting.user_starred_projects_api_limit).to eq(100) } + it { expect(setting.users_api_limit_followers).to eq(100) } + it { expect(setting.users_api_limit_following).to eq(100) } + it { expect(setting.users_api_limit_status).to eq(240) } + it { expect(setting.users_api_limit_ssh_keys).to eq(120) } + it { expect(setting.users_api_limit_ssh_key).to eq(120) } + it { expect(setting.users_api_limit_gpg_keys).to eq(120) } + it { expect(setting.users_api_limit_gpg_key).to eq(120) } it { expect(setting.disable_password_authentication_for_users_with_sso_identities).to be(false) } it { expect(setting.root_moved_permanently_redirection).to be(false) } it { expect(setting.resource_usage_limits).to eq({}) } @@ -293,6 +300,13 @@ def many_usernames(num = 100) user_projects_api_limit user_starred_projects_api_limit users_get_by_id_limit + users_api_limit_followers + users_api_limit_following + users_api_limit_status + users_api_limit_ssh_keys + users_api_limit_ssh_key + users_api_limit_gpg_keys + users_api_limit_gpg_key ] end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c856ff124b03d3c141dd107359ad555fe4185872..8c8f1902b9bca0d53a576493f612819d3e86ecc9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -2595,7 +2595,7 @@ def update_password(user, admin, password = User.random_password) end context 'when user is authenticated' do - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_keys do + it_behaves_like 'rate limited endpoint', rate_limit_key: :user_ssh_keys do def request get api(path, current_user) end @@ -2605,7 +2605,7 @@ def request context 'when user is unauthenticated' do let(:current_user) { nil } - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_keys do + it_behaves_like 'rate limited endpoint', rate_limit_key: :user_ssh_keys do def request get api(path, current_user) end @@ -2643,7 +2643,7 @@ def request context 'when the rate limit has been reached' do it 'returns status 429 Too Many Requests', :aggregate_failures do ip = '1.2.3.4' - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:user_keys, scope: ip).and_return(true) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:user_ssh_keys, scope: ip).and_return(true) get api(path), env: { REMOTE_ADDR: ip } @@ -2690,7 +2690,7 @@ def request end context 'when user is authenticated' do - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_specific_key do + it_behaves_like 'rate limited endpoint', rate_limit_key: :user_ssh_key do def request get api(path, current_user) end @@ -2700,7 +2700,7 @@ def request context 'when user is unauthenticated' do let(:current_user) { nil } - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_specific_key do + it_behaves_like 'rate limited endpoint', rate_limit_key: :user_ssh_key do def request get api(path, current_user) end @@ -2871,7 +2871,7 @@ def request end context 'when user is authenticated' do - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_specific_gpg_key do + it_behaves_like 'rate limited endpoint', rate_limit_key: :user_gpg_key do def request get api(path, current_user) end @@ -2881,7 +2881,7 @@ def request context 'when user is unauthenticated' do let(:current_user) { nil } - it_behaves_like 'rate limited endpoint', rate_limit_key: :user_specific_gpg_key do + it_behaves_like 'rate limited endpoint', rate_limit_key: :user_gpg_key do def request get api(path, current_user) end diff --git a/spec/views/admin/application_settings/network.html.haml_spec.rb b/spec/views/admin/application_settings/network.html.haml_spec.rb index 25995fb6929cc7314ad94a2e0354d8e9532bb049..be298123b480adc75f5b261aa39343f00b07ee00 100644 --- a/spec/views/admin/application_settings/network.html.haml_spec.rb +++ b/spec/views/admin/application_settings/network.html.haml_spec.rb @@ -21,6 +21,29 @@ end end + context 'for Users API rate limits' do + it 'renders the reset disclaimer' do + render + + expect(rendered).to have_content('Set to 0 to disable the limits.') + end + + it 'renders the users rate limit fields', :aggregate_failures do + render + + expect(rendered).to have_field('application_setting_users_api_limit_followers') + expect(rendered).to have_field('application_setting_users_api_limit_following') + expect(rendered).to have_field('application_setting_users_api_limit_status') + expect(rendered).to have_field('application_setting_users_api_limit_ssh_keys') + expect(rendered).to have_field('application_setting_users_api_limit_ssh_key') + expect(rendered).to have_field('application_setting_users_api_limit_gpg_keys') + expect(rendered).to have_field('application_setting_users_api_limit_gpg_key') + + expect(rendered).to have_field('application_setting_users_get_by_id_limit') + expect(rendered).to have_field('application_setting_users_get_by_id_limit_allowlist_raw') + end + end + context 'for Projects API rate limits' do it 'renders the project rate limit fields' do render