Commit 134fe182 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 6a7cc8c1
...@@ -5,9 +5,11 @@ import initNotes from '~/init_notes'; ...@@ -5,9 +5,11 @@ import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed'; import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new if (!gon.features.snippetsVue) {
new BlobViewer(); // eslint-disable-line no-new new LineHighlighter(); // eslint-disable-line no-new
initNotes(); new BlobViewer(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new initNotes();
snippetEmbed(); new ZenMode(); // eslint-disable-line no-new
snippetEmbed();
}
}); });
/*
* These styles are specific to the gl-toast component.
* Documentation: https://design.gitlab.com/components/toasts
* Note: Styles below are nested in order to override some of vue-toasted's default styling
*/
.toasted-container {
max-width: $toast-max-width;
@include media-breakpoint-down(xs) {
width: 100%;
padding-right: $toast-padding-right;
}
.toasted.gl-toast {
border-radius: $border-radius-default;
font-size: $gl-font-size;
padding: $gl-padding-8 $gl-padding $gl-padding-8 $gl-padding-24;
margin-top: $toast-default-margin;
line-height: $gl-line-height;
background-color: rgba($gray-900, $toast-background-opacity);
span {
padding-right: $gl-padding-8;
}
@include media-breakpoint-down(xs) {
.action:first-of-type {
// Ensures actions buttons are right aligned on mobile
margin-left: auto;
}
}
.action {
color: $blue-300;
margin: 0 0 0 $toast-default-margin;
text-transform: none;
font-size: $gl-font-size;
}
.toast-close {
font-size: $default-icon-size;
margin-left: $toast-default-margin;
}
}
}
// Overrides the default positioning of toasts
body .toasted-container.bottom-left {
bottom: $toast-offset;
left: $toast-offset;
}
...@@ -528,16 +528,6 @@ $pagination-padding-x: 16px; ...@@ -528,16 +528,6 @@ $pagination-padding-x: 16px;
$pagination-line-height: 20px; $pagination-line-height: 20px;
$pagination-disabled-color: #cdcdcd; $pagination-disabled-color: #cdcdcd;
/*
* Toasts
*/
$toast-offset: 24px;
$toast-height: 48px;
$toast-max-width: 586px;
$toast-padding-right: 42px;
$toast-default-margin: 8px;
$toast-background-opacity: 0.95;
/* /*
* Status icons * Status icons
*/ */
......
...@@ -83,12 +83,14 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController ...@@ -83,12 +83,14 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def play_rate_limit def play_rate_limit
return unless current_user return unless current_user
limiter = ::Gitlab::ActionRateLimiter.new(action: :play_pipeline_schedule) if rate_limiter.throttled?(:play_pipeline_schedule, scope: [current_user, schedule])
flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.')
return unless limiter.throttled?([current_user, schedule], 1) redirect_to pipeline_schedules_path(@project)
end
end
flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.') def rate_limiter
redirect_to pipeline_schedules_path(@project) ::Gitlab::ApplicationRateLimiter
end end
def schedule def schedule
......
...@@ -19,14 +19,16 @@ class Projects::RawController < Projects::ApplicationController ...@@ -19,14 +19,16 @@ class Projects::RawController < Projects::ApplicationController
private private
def show_rate_limit def show_rate_limit
limiter = ::Gitlab::ActionRateLimiter.new(action: :show_raw_controller) if rate_limiter.throttled?(:show_raw_controller, scope: [@project, @commit, @path], threshold: raw_blob_request_limit)
rate_limiter.log_request(request, :raw_blob_request_limit, current_user)
return unless limiter.throttled?([@project, @commit, @path], raw_blob_request_limit) flash[:alert] = _('You cannot access the raw file. Please wait a minute.')
redirect_to project_blob_path(@project, File.join(@ref, @path)), status: :too_many_requests
limiter.log_request(request, :raw_blob_request_limit, current_user) end
end
flash[:alert] = _('You cannot access the raw file. Please wait a minute.') def rate_limiter
redirect_to project_blob_path(@project, File.join(@ref, @path)), status: :too_many_requests ::Gitlab::ApplicationRateLimiter
end end
def raw_blob_request_limit def raw_blob_request_limit
......
...@@ -32,6 +32,9 @@ class ProjectsController < Projects::ApplicationController ...@@ -32,6 +32,9 @@ class ProjectsController < Projects::ApplicationController
before_action :authorize_archive_project!, only: [:archive, :unarchive] before_action :authorize_archive_project!, only: [:archive, :unarchive]
before_action :event_filter, only: [:show, :activity] before_action :event_filter, only: [:show, :activity]
# Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
layout :determine_layout layout :determine_layout
def index def index
...@@ -465,6 +468,21 @@ class ProjectsController < Projects::ApplicationController ...@@ -465,6 +468,21 @@ class ProjectsController < Projects::ApplicationController
def present_project def present_project
@project = @project.present(current_user: current_user) @project = @project.present(current_user: current_user)
end end
def export_rate_limit
prefixed_action = "project_#{params[:action]}".to_sym
if rate_limiter.throttled?(prefixed_action, scope: [current_user, prefixed_action, @project])
rate_limiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
redirect_to edit_project_path(@project)
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end end
ProjectsController.prepend_if_ee('EE::ProjectsController') ProjectsController.prepend_if_ee('EE::ProjectsController')
...@@ -4,13 +4,16 @@ ...@@ -4,13 +4,16 @@
- breadcrumb_title @snippet.to_reference - breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") - page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
= render 'shared/snippets/header' - if Feature.enabled?(:snippets_vue)
#js-snippet-view{ 'data-qa-selector': 'snippet_view' }
- else
= render 'shared/snippets/header'
.personal-snippets .personal-snippets
%article.file-holder.snippet-file-content %article.file-holder.snippet-file-content
= render 'shared/snippets/blob' = render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block .row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true = render 'award_emoji/awards_block', awardable: @snippet, inline: true
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false
---
title: Add Project Export request/download rate limits
merge_request: 20962
author:
type: other
---
title: Remove redundant toast.scss file and variables
merge_request: 21105
author:
type: fixed
...@@ -288,6 +288,12 @@ The PgBouncer exporter allows you to measure various PgBouncer metrics. ...@@ -288,6 +288,12 @@ The PgBouncer exporter allows you to measure various PgBouncer metrics.
[➔ Read more about the PgBouncer exporter.](pgbouncer_exporter.md) [➔ Read more about the PgBouncer exporter.](pgbouncer_exporter.md)
### Registry exporter
The Registry exporter allows you to measure various Registry metrics.
[➔ Read more about the Registry exporter.](registry_exporter.md)
### GitLab exporter ### GitLab exporter
The GitLab exporter allows you to measure various GitLab metrics, pulled from Redis and the database. The GitLab exporter allows you to measure various GitLab metrics, pulled from Redis and the database.
......
...@@ -8,20 +8,54 @@ The [postgres exporter] allows you to measure various PostgreSQL metrics. ...@@ -8,20 +8,54 @@ The [postgres exporter] allows you to measure various PostgreSQL metrics.
To enable the postgres exporter: To enable the postgres exporter:
1. [Enable Prometheus](index.md#configuring-prometheus) 1. [Enable Prometheus](index.md#configuring-prometheus).
1. Edit `/etc/gitlab/gitlab.rb` 1. Edit `/etc/gitlab/gitlab.rb` and enable `postgres_exporter`:
1. Add or find and uncomment the following line, making sure it's set to `true`:
```ruby ```ruby
postgres_exporter['enable'] = true postgres_exporter['enable'] = true
``` ```
NOTE: **Note:**
If PostgreSQL is configured on a separate node, make sure that the local
address is [listed in `trust_auth_cidr_addresses`](../../high_availability/database.md#network-information) or the
exporter will not be able to connect to the database.
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect take effect.
Prometheus will now automatically begin collecting performance data from Prometheus will now automatically begin collecting performance data from
the postgres exporter exposed under `localhost:9187`. the postgres exporter exposed under `localhost:9187`.
## Advanced configuration
In most cases, Postgres exporter will work with the defaults and you should not
need to change anything.
The following configuration options can be used to further customize the
Postgres exporter:
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
postgres_exporter['dbname'] = 'pgbouncer' # The name of the database to connect to.
postgres_exporter['user'] = 'gitlab-psql' # The user to sign in as.
postgres_exporter['password'] = '' # The user's password.
postgres_exporter['host'] = 'localhost' # The host to connect to. Values that start with '/' are for unix domain sockets (default is 'localhost').
postgres_exporter['port'] = 5432 # The port to bind to (default is '5432').
postgres_exporter['sslmode'] = 'require' # Whether or not to use SSL. Valid options are:
# 'disable' (no SSL),
# 'require' (always use SSL and skip verification, this is the default value),
# 'verify-ca' (always use SSL and verify that the certificate presented by the server was signed by a trusted CA),
# 'verify-full' (always use SSL and verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate).
postgres_exporter['fallback_application_name'] = '' # An application_name to fall back to if one isn't provided.
postgres_exporter['connect_timeout'] = '' # Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.
postgres_exporter['sslcert'] = 'ssl.crt' # Cert file location. The file must contain PEM encoded data.
postgres_exporter['sslkey'] = 'ssl.key' # Key file location. The file must contain PEM encoded data.
postgres_exporter['sslrootcert'] = 'ssl-root.crt' # The location of the root certificate file. The file must contain PEM encoded data.
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to take effect.
[← Back to the main Prometheus page](index.md) [← Back to the main Prometheus page](index.md)
[1131]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1131 [1131]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1131
......
# Registry exporter
> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2884) in GitLab 11.9.
The Registry exporter allows you to measure various Registry metrics.
To enable it:
1. [Enable Prometheus](index.md#configuring-prometheus).
1. Edit `/etc/gitlab/gitlab.rb` and enable [debug mode](https://docs.docker.com/registry/#debug) for the Registry:
```ruby
registry['debug_addr'] = "localhost:5001" # localhost:5001/metrics
```
1. Save the file and [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect.
Prometheus will now automatically begin collecting performance data from
the registry exporter exposed under `localhost:5001/metrics`.
[← Back to the main Prometheus page](index.md)
...@@ -129,3 +129,9 @@ In the rails console (`rails c`), enter the following command to enable your fea ...@@ -129,3 +129,9 @@ In the rails console (`rails c`), enter the following command to enable your fea
```ruby ```ruby
Feature.enable(:feature_flag_name) Feature.enable(:feature_flag_name)
``` ```
Similarly, the following command will disable a feature flag:
```ruby
Feature.disable(:feature_flag_name)
```
...@@ -71,7 +71,7 @@ Once you're on the dashboard, at the top you should see a series of filters for: ...@@ -71,7 +71,7 @@ Once you're on the dashboard, at the top you should see a series of filters for:
- Report type - Report type
- Project - Project
To the right of the filters, you should see a **Hide dismissed** toggle button ([available for GitLab.com Gold, planned for GitLab Ultimate 12.6](https://gitlab.com/gitlab-org/gitlab/issues/9102)). To the right of the filters, you should see a **Hide dismissed** toggle button.
NOTE: **Note:** NOTE: **Note:**
The dashboard only shows projects with [security reports](#supported-reports) enabled in a group. The dashboard only shows projects with [security reports](#supported-reports) enabled in a group.
......
...@@ -107,6 +107,8 @@ installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default ...@@ -107,6 +107,8 @@ installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1. region of the VMs is US East1.
Each instance is used only for one job, this ensures any sensitive data left on the system can't be accessed by other people their CI jobs. Each instance is used only for one job, this ensures any sensitive data left on the system can't be accessed by other people their CI jobs.
The `gitlab-shared-runners-manager-X.gitlab.com` fleet of Runners are dedicated for GitLab projects as well as community forks of them. They use a slightly larger machine type (n1-standard-2) and have a bigger SSD disk size. They will not run untagged jobs and unlike the general fleet of shared Runners, the instances are re-used up to 40 times.
Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`), Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`),
**will be timed out after 3 hours**, regardless of the timeout configured in a **will be timed out after 3 hours**, regardless of the timeout configured in a
project. Check the issues [4010] and [4070] for the reference. project. Check the issues [4010] and [4070] for the reference.
......
...@@ -144,7 +144,6 @@ project or branch name. Special characters can include: ...@@ -144,7 +144,6 @@ project or branch name. Special characters can include:
- Leading underscore - Leading underscore
- Trailing hyphen/dash - Trailing hyphen/dash
- Double hyphen/dash
To get around this, you can [change the group path](../../group/index.md#changing-a-groups-path), To get around this, you can [change the group path](../../group/index.md#changing-a-groups-path),
[change the project path](../../project/settings/index.md#renaming-a-repository) or change the branch [change the project path](../../project/settings/index.md#renaming-a-repository) or change the branch
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
With the GitLab [Maven](https://maven.apache.org) Repository, every With the GitLab [Maven](https://maven.apache.org) Repository, every
project can have its own space to store its Maven artifacts. project can have its own space to store its Maven artifacts.
![GitLab Maven Repository](img/maven_package_view.png) ![GitLab Maven Repository](img/maven_package_view_v12_6.png)
## Enabling the Maven Repository ## Enabling the Maven Repository
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
With the GitLab NPM Registry, every With the GitLab NPM Registry, every
project can have its own space to store NPM packages. project can have its own space to store NPM packages.
![GitLab NPM Registry](img/npm_package_view.png) ![GitLab NPM Registry](img/npm_package_view_v12_5.png)
NOTE: **Note:** NOTE: **Note:**
Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported. Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported.
...@@ -42,6 +42,20 @@ it is not possible due to a naming collision. For example: ...@@ -42,6 +42,20 @@ it is not possible due to a naming collision. For example:
| `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes | | `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes |
| `gitlab-org/gitlab` | `@foo/bar` | No | | `gitlab-org/gitlab` | `@foo/bar` | No |
The regex that is used for naming is validating all package names from all package managers:
```
/\A\@?(([\w\-\.\+]*)\/)*([\w\-\.]+)@?(([\w\-\.\+]*)\/)*([\w\-\.]*)\z/
```
It allows for capital letters, while NPM does not, and allows for almost all of the
characters NPM allows with a few exceptions (`~` is not allowed).
NOTE: **Note:** Capital letters are needed because the scope is required to be
identical to the top level namespace of the project. So, for example, if your
project path is `My-Group/project-foo`, your package must be named `@My-Group/any-package-name`.
`@my-group/any-package-name` will not work.
CAUTION: **When updating the path of a user/group or transferring a (sub)group/project:** CAUTION: **When updating the path of a user/group or transferring a (sub)group/project:**
If you update the root namespace of a project with NPM packages, your changes will be rejected. To be allowed to do that, make sure to remove any NPM package first. Don't forget to update your `.npmrc` files to follow the above naming convention and run `npm publish` if necessary. If you update the root namespace of a project with NPM packages, your changes will be rejected. To be allowed to do that, make sure to remove any NPM package first. Don't forget to update your `.npmrc` files to follow the above naming convention and run `npm publish` if necessary.
......
...@@ -1285,7 +1285,7 @@ Markdown features, like link labels. ...@@ -1285,7 +1285,7 @@ Markdown features, like link labels.
## Testing webhooks ## Testing webhooks
You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project. You can trigger the webhook manually. Sample data from the project will be used. Sample data will take from the project.
> For example: for triggering `Push Events` your project should have at least one commit. > For example: for triggering `Push Events` your project should have at least one commit.
![Webhook testing](img/webhook_testing.png) ![Webhook testing](img/webhook_testing.png)
......
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
module API module API
class ProjectExport < Grape::API class ProjectExport < Grape::API
helpers do
def throttled?(action)
rate_limiter.throttled?(action, scope: [current_user, action, user_project])
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end
before do before do
not_found! unless Gitlab::CurrentSettings.project_export_enabled? not_found! unless Gitlab::CurrentSettings.project_export_enabled?
authorize_admin_project authorize_admin_project
...@@ -23,6 +32,10 @@ module API ...@@ -23,6 +32,10 @@ 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
if throttled?(:project_download_export)
render_api_error!({ error: 'This endpoint has been requested too many times. Try again later.' }, 429)
end
if user_project.export_file_exists? if user_project.export_file_exists?
present_carrierwave_file!(user_project.export_file) present_carrierwave_file!(user_project.export_file)
else else
...@@ -41,6 +54,10 @@ module API ...@@ -41,6 +54,10 @@ module API
end end
end end
post ':id/export' do post ':id/export' do
if throttled?(:project_export)
render_api_error!({ error: 'This endpoint has been requested too many times. Try again later.' }, 429)
end
project_export_params = declared_params(include_missing: false) project_export_params = declared_params(include_missing: false)
after_export_params = project_export_params.delete(:upload) || {} after_export_params = project_export_params.delete(:upload) || {}
......
# frozen_string_literal: true
module Gitlab
# This class implements a simple rate limiter that can be used to throttle
# certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
# the middleware level, this can be used at the controller level.
class ActionRateLimiter
TIME_TO_EXPIRE = 60 # 1 min
attr_accessor :action, :expiry_time
def initialize(action:, expiry_time: TIME_TO_EXPIRE)
@action = action
@expiry_time = expiry_time
end
# Increments the given cache key and increments the value by 1 with the
# given expiration time. Returns the incremented value.
#
# key - An array of ActiveRecord instances
def increment(key)
value = 0
Gitlab::Redis::Cache.with do |redis|
cache_key = action_key(key)
value = redis.incr(cache_key)
redis.expire(cache_key, expiry_time) if value == 1
end
value
end
# Increments the given key and returns true if the action should
# be throttled.
#
# key - An array of ActiveRecord instances or strings
# threshold_value - The maximum number of times this action should occur in the given time interval. If number is zero is considered disabled.
def throttled?(key, threshold_value)
threshold_value > 0 &&
self.increment(key) > threshold_value
end
# Logs request into auth.log
#
# request - Web request to be logged
# type - A symbol key that represents the request.
# current_user - Current user of the request, it can be nil.
def log_request(request, type, current_user)
request_information = {
message: 'Action_Rate_Limiter_Request',
env: type,
remote_ip: request.ip,
request_method: request.request_method,
path: request.fullpath
}
if current_user
request_information.merge!({
user_id: current_user.id,
username: current_user.username
})
end
Gitlab::AuthLogger.error(request_information)
end
private
def action_key(key)
serialized = key.map do |obj|
if obj.is_a?(String)
"#{obj}"
else
"#{obj.class.model_name.to_s.underscore}:#{obj.id}"
end
end.join(":")
"action_rate_limiter:#{action}:#{serialized}"
end
end
end
# frozen_string_literal: true
module Gitlab
# This class implements a simple rate limiter that can be used to throttle
# certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
# the middleware level, this can be used at the controller or API level.
#
# @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 << self
# Application rate limits
#
# Threshold value can be either an Integer or a Proc
# in order to not evaluate it's value every time this method is called
# and only do that when it's needed.
def rate_limits
{
project_export: { threshold: 1, interval: 5.minutes },
project_download_export: { threshold: 10, interval: 10.minutes },
project_generate_new_export: { threshold: 1, interval: 5.minutes },
play_pipeline_schedule: { threshold: 1, interval: 1.minute },
show_raw_controller: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.raw_blob_request_limit }, interval: 1.minute }
}.freeze
end
# Increments the given key and returns true if the action should
# be throttled.
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
# @option threshold [Integer] Optional threshold value to override default one registered in `.rate_limits`
# @option interval [Integer] Optional interval value to override default one registered in `.rate_limits`
#
# @return [Boolean] Whether or not a request should be throttled
def throttled?(key, scope: nil, interval: nil, threshold: nil)
return unless rate_limits[key]
threshold_value = threshold || threshold(key)
threshold_value > 0 &&
increment(key, scope, interval) > threshold_value
end
# Increments the given cache key and increments the value by 1 with the
# expiration interval defined in `.rate_limits`.
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
# @option interval [Integer] Optional interval value to override default one registered in `.rate_limits`
#
# @return [Integer] incremented value
def increment(key, scope, interval = nil)
value = 0
interval_value = interval || interval(key)
Gitlab::Redis::Cache.with do |redis|
cache_key = action_key(key, scope)
value = redis.incr(cache_key)
redis.expire(cache_key, interval_value) if value == 1
end
value
end
# Logs request using provided logger
#
# @param request [Http::Request] - Web request to be logged
# @param type [Symbol] A symbol key that represents the request
# @param current_user [User] Current user of the request, it can be nil
# @param logger [Logger] Logger to log request to a specific log file. Defaults to Gitlab::AuthLogger
def log_request(request, type, current_user, logger = Gitlab::AuthLogger)
request_information = {
message: 'Application_Rate_Limiter_Request',
env: type,
remote_ip: request.ip,
request_method: request.request_method,
path: request.fullpath
}
if current_user
request_information.merge!({
user_id: current_user.id,
username: current_user.username
})
end
logger.error(request_information)
end
private
def threshold(key)
value = rate_limit_value_by_key(key, :threshold)
return value.call if value.is_a?(Proc)
value.to_i
end
def interval(key)
rate_limit_value_by_key(key, :interval).to_i
end
def rate_limit_value_by_key(key, setting)
action = rate_limits[key]
action[setting] if action
end
def action_key(key, scope)
composed_key = [key, scope].flatten.compact
serialized = composed_key.map do |obj|
if obj.is_a?(String) || obj.is_a?(Symbol)
"#{obj}"
else
"#{obj.class.model_name.to_s.underscore}:#{obj.id}"
end
end.join(":")
"application_rate_limiter:#{serialized}"
end
end
end
end
...@@ -42,6 +42,7 @@ module Gitlab ...@@ -42,6 +42,7 @@ module Gitlab
# Initialize gon.features with any flags that should be # Initialize gon.features with any flags that should be
# made globally available to the frontend # made globally available to the frontend
push_frontend_feature_flag(:suppress_ajax_navigation_errors, default_enabled: true) push_frontend_feature_flag(:suppress_ajax_navigation_errors, default_enabled: true)
push_frontend_feature_flag(:snippets_vue, default_enabled: false)
end end
# Exposes the state of a feature flag to the frontend code. # Exposes the state of a feature flag to the frontend code.
......
...@@ -17898,6 +17898,9 @@ msgstr "" ...@@ -17898,6 +17898,9 @@ msgstr ""
msgid "This domain is not verified. You will need to verify ownership before access is enabled." msgid "This domain is not verified. You will need to verify ownership before access is enabled."
msgstr "" msgstr ""
msgid "This endpoint has been requested too many times. Try again later."
msgstr ""
msgid "This environment has no deployments yet." msgid "This environment has no deployments yet."
msgstr "" msgstr ""
......
...@@ -65,7 +65,7 @@ describe Projects::RawController do ...@@ -65,7 +65,7 @@ describe Projects::RawController do
it 'logs the event on auth.log' do it 'logs the event on auth.log' do
attributes = { attributes = {
message: 'Action_Rate_Limiter_Request', message: 'Application_Rate_Limiter_Request',
env: :raw_blob_request_limit, env: :raw_blob_request_limit,
remote_ip: '0.0.0.0', remote_ip: '0.0.0.0',
request_method: 'GET', request_method: 'GET',
......
...@@ -1055,45 +1055,34 @@ describe ProjectsController do ...@@ -1055,45 +1055,34 @@ describe ProjectsController do
end end
end end
describe '#export' do describe 'project export' do
before do before do
sign_in(user) sign_in(user)
project.add_maintainer(user) project.add_maintainer(user)
end end
context 'when project export is enabled' do shared_examples 'rate limits project export endpoint' do
it 'returns 302' do
get :export, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(302)
end
end
context 'when project export is disabled' do
before do before do
stub_application_setting(project_export_enabled?: false) allow(::Gitlab::ApplicationRateLimiter)
.to receive(:throttled?)
.and_return(true)
end end
it 'returns 404' do it 'prevents requesting project export' do
get :export, params: { namespace_id: project.namespace, id: project } get action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(404) expect(flash[:alert]).to eq('This endpoint has been requested too many times. Try again later.')
expect(response).to have_gitlab_http_status(302)
end end
end end
end
describe '#download_export' do describe '#export' do
before do let(:action) { :export }
sign_in(user)
project.add_maintainer(user)
end
context 'object storage enabled' do
context 'when project export is enabled' do context 'when project export is enabled' do
it 'returns 302' do it 'returns 302' do
get :download_export, params: { namespace_id: project.namespace, id: project } get action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
end end
...@@ -1105,66 +1094,96 @@ describe ProjectsController do ...@@ -1105,66 +1094,96 @@ describe ProjectsController do
end end
it 'returns 404' do it 'returns 404' do
get :download_export, params: { namespace_id: project.namespace, id: project } get action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
end end
context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do
include_examples 'rate limits project export endpoint'
end
end end
end
describe '#remove_export' do describe '#download_export' do
before do let(:action) { :download_export }
sign_in(user)
project.add_maintainer(user) context 'object storage enabled' do
end context 'when project export is enabled' do
it 'returns 302' do
get action, params: { namespace_id: project.namespace, id: project }
context 'when project export is enabled' do expect(response).to have_gitlab_http_status(302)
it 'returns 302' do end
post :remove_export, params: { namespace_id: project.namespace, id: project } end
expect(response).to have_gitlab_http_status(302) context 'when project export is disabled' do
end before do
end stub_application_setting(project_export_enabled?: false)
end
context 'when project export is disabled' do it 'returns 404' do
before do get action, params: { namespace_id: project.namespace, id: project }
stub_application_setting(project_export_enabled?: false)
end
it 'returns 404' do expect(response).to have_gitlab_http_status(404)
post :remove_export, params: { namespace_id: project.namespace, id: project } end
end
expect(response).to have_gitlab_http_status(404) context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do
include_examples 'rate limits project export endpoint'
end
end end
end end
end
describe '#generate_new_export' do describe '#remove_export' do
before do let(:action) { :remove_export }
sign_in(user)
project.add_maintainer(user) context 'when project export is enabled' do
end it 'returns 302' do
post action, params: { namespace_id: project.namespace, id: project }
context 'when project export is enabled' do expect(response).to have_gitlab_http_status(302)
it 'returns 302' do end
post :generate_new_export, params: { namespace_id: project.namespace, id: project } end
expect(response).to have_gitlab_http_status(302) context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
end
it 'returns 404' do
post action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(404)
end
end end
end end
context 'when project export is disabled' do describe '#generate_new_export' do
before do let(:action) { :generate_new_export }
stub_application_setting(project_export_enabled?: false)
context 'when project export is enabled' do
it 'returns 302' do
post action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(302)
end
end end
it 'returns 404' do context 'when project export is disabled' do
post :generate_new_export, params: { namespace_id: project.namespace, id: project } before do
stub_application_setting(project_export_enabled?: false)
end
expect(response).to have_gitlab_http_status(404) it 'returns 404' do
post action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(404)
end
end
context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do
include_examples 'rate limits project export endpoint'
end end
end end
end end
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
describe 'Internal Snippets', :js do describe 'Internal Snippets', :js do
let(:internal_snippet) { create(:personal_snippet, :internal) } let(:internal_snippet) { create(:personal_snippet, :internal) }
before do
stub_feature_flags(snippets_vue: false)
end
describe 'normal user' do describe 'normal user' do
before do before do
sign_in(create(:user)) sign_in(create(:user))
......
...@@ -16,6 +16,7 @@ describe 'Comments on personal snippets', :js do ...@@ -16,6 +16,7 @@ describe 'Comments on personal snippets', :js do
let!(:other_note) { create(:note_on_personal_snippet) } let!(:other_note) { create(:note_on_personal_snippet) }
before do before do
stub_feature_flags(snippets_vue: false)
sign_in user sign_in user
visit snippet_path(snippet) visit snippet_path(snippet)
......
...@@ -6,6 +6,7 @@ describe 'Private Snippets', :js do ...@@ -6,6 +6,7 @@ describe 'Private Snippets', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(snippets_vue: false)
sign_in(user) sign_in(user)
end end
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe 'Public Snippets', :js do describe 'Public Snippets', :js do
before do
stub_feature_flags(snippets_vue: false)
end
it 'Unauthenticated user should see public snippets' do it 'Unauthenticated user should see public snippets' do
public_snippet = create(:personal_snippet, :public) public_snippet = create(:personal_snippet, :public)
......
...@@ -6,6 +6,10 @@ describe 'Snippet', :js do ...@@ -6,6 +6,10 @@ describe 'Snippet', :js do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) } let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
before do
stub_feature_flags(snippets_vue: false)
end
context 'Ruby file' do context 'Ruby file' do
let(:file_name) { 'popen.rb' } let(:file_name) { 'popen.rb' }
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data } let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
......
...@@ -7,6 +7,7 @@ describe 'User creates snippet', :js do ...@@ -7,6 +7,7 @@ describe 'User creates snippet', :js do
before do before do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
stub_feature_flags(snippets_vue: false)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!( Gitlab::CurrentSettings.update!(
......
...@@ -8,6 +8,7 @@ describe 'User creates snippet', :js do ...@@ -8,6 +8,7 @@ describe 'User creates snippet', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(snippets_vue: false)
sign_in(user) sign_in(user)
visit new_snippet_path visit new_snippet_path
end end
......
...@@ -10,6 +10,8 @@ describe 'User deletes snippet' do ...@@ -10,6 +10,8 @@ describe 'User deletes snippet' do
before do before do
sign_in(user) sign_in(user)
stub_feature_flags(snippets_vue: false)
visit snippet_path(snippet) visit snippet_path(snippet)
end end
......
...@@ -12,6 +12,7 @@ describe 'User edits snippet', :js do ...@@ -12,6 +12,7 @@ describe 'User edits snippet', :js do
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) } let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) }
before do before do
stub_feature_flags(snippets_vue: false)
sign_in(user) sign_in(user)
visit edit_snippet_path(snippet) visit edit_snippet_path(snippet)
......
...@@ -6,11 +6,38 @@ describe 'Snippets' do ...@@ -6,11 +6,38 @@ describe 'Snippets' do
context 'when the project has snippets' do context 'when the project has snippets' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do before do
allow(Snippet).to receive(:default_per_page).and_return(1) allow(Snippet).to receive(:default_per_page).and_return(1)
visit snippets_path(username: project.owner.username)
visit project_snippets_path(project)
end end
it_behaves_like 'paginated snippets' it_behaves_like 'paginated snippets'
end end
describe 'rendering engine' do
let_it_be(:snippet) { create(:personal_snippet, :public) }
let(:snippets_vue_feature_flag_enabled) { true }
before do
stub_feature_flags(snippets_vue: snippets_vue_feature_flag_enabled)
visit snippet_path(snippet)
end
it 'renders Vue application' do
expect(page).to have_selector('#js-snippet-view')
expect(page).not_to have_selector('.personal-snippets')
end
context 'when feature flag is disabled' do
let(:snippets_vue_feature_flag_enabled) { false }
it 'renders HAML application and not Vue' do
expect(page).not_to have_selector('#js-snippet-view')
expect(page).to have_selector('.personal-snippets')
end
end
end
end end
...@@ -2,30 +2,40 @@ ...@@ -2,30 +2,40 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_cache do
let(:redis) { double('redis') } let(:redis) { double('redis') }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:rate_limits) do
{
test_action: {
threshold: 1,
interval: 2.minutes
}
}
end
let(:key) { rate_limits.keys[0] }
subject { described_class.new(action: :test_action, expiry_time: 100) } subject { described_class }
before do before do
allow(Gitlab::Redis::Cache).to receive(:with).and_yield(redis) allow(Gitlab::Redis::Cache).to receive(:with).and_yield(redis)
allow(described_class).to receive(:rate_limits).and_return(rate_limits)
end end
shared_examples 'action rate limiter' do shared_examples 'action rate limiter' do
it 'increases the throttle count and sets the expiration time' do it 'increases the throttle count and sets the expiration time' do
expect(redis).to receive(:incr).with(cache_key).and_return(1) expect(redis).to receive(:incr).with(cache_key).and_return(1)
expect(redis).to receive(:expire).with(cache_key, 100) expect(redis).to receive(:expire).with(cache_key, 120)
expect(subject.throttled?(key, 1)).to be_falsy expect(subject.throttled?(key, scope: scope)).to be_falsy
end end
it 'returns true if the key is throttled' do it 'returns true if the key is throttled' do
expect(redis).to receive(:incr).with(cache_key).and_return(2) expect(redis).to receive(:incr).with(cache_key).and_return(2)
expect(redis).not_to receive(:expire) expect(redis).not_to receive(:expire)
expect(subject.throttled?(key, 1)).to be_truthy expect(subject.throttled?(key, scope: scope)).to be_truthy
end end
context 'when throttling is disabled' do context 'when throttling is disabled' do
...@@ -33,16 +43,16 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do ...@@ -33,16 +43,16 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do
expect(redis).not_to receive(:incr) expect(redis).not_to receive(:incr)
expect(redis).not_to receive(:expire) expect(redis).not_to receive(:expire)
expect(subject.throttled?(key, 0)).to be_falsy expect(subject.throttled?(key, scope: scope, threshold: 0)).to be_falsy
end end
end end
end end
context 'when the key is an array of only ActiveRecord models' do context 'when the key is an array of only ActiveRecord models' do
let(:key) { [user, project] } let(:scope) { [user, project] }
let(:cache_key) do let(:cache_key) do
"action_rate_limiter:test_action:user:#{user.id}:project:#{project.id}" "application_rate_limiter:test_action:user:#{user.id}:project:#{project.id}"
end end
it_behaves_like 'action rate limiter' it_behaves_like 'action rate limiter'
...@@ -52,10 +62,10 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do ...@@ -52,10 +62,10 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:commit) { project.repository.commit } let(:commit) { project.repository.commit }
let(:path) { 'app/controllers/groups_controller.rb' } let(:path) { 'app/controllers/groups_controller.rb' }
let(:key) { [project, commit, path] } let(:scope) { [project, commit, path] }
let(:cache_key) do let(:cache_key) do
"action_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}:#{path}" "application_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}:#{path}"
end end
it_behaves_like 'action rate limiter' it_behaves_like 'action rate limiter'
...@@ -72,7 +82,7 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do ...@@ -72,7 +82,7 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do
let(:base_attributes) do let(:base_attributes) do
{ {
message: 'Action_Rate_Limiter_Request', message: 'Application_Rate_Limiter_Request',
env: type, env: type,
remote_ip: '127.0.0.1', remote_ip: '127.0.0.1',
request_method: 'GET', request_method: 'GET',
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe API::ProjectExport do describe API::ProjectExport, :clean_gitlab_redis_cache do
set(:project) { create(:project) } set(:project) { create(:project) }
set(:project_none) { create(:project) } set(:project_none) { create(:project) }
set(:project_started) { create(:project) } set(:project_started) { create(:project) }
...@@ -47,6 +47,19 @@ describe API::ProjectExport do ...@@ -47,6 +47,19 @@ describe API::ProjectExport do
it_behaves_like '404 response' it_behaves_like '404 response'
end end
shared_examples_for 'when rate limit is exceeded' do
before do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
end
it 'prevents requesting project export' do
request
expect(response).to have_gitlab_http_status(429)
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end
end
describe 'GET /projects/:project_id/export' do describe 'GET /projects/:project_id/export' do
shared_examples_for 'get project export status not found' do shared_examples_for 'get project export status not found' do
it_behaves_like '404 response' do it_behaves_like '404 response' do
...@@ -219,6 +232,12 @@ describe API::ProjectExport do ...@@ -219,6 +232,12 @@ describe API::ProjectExport do
let(:user) { admin } let(:user) { admin }
it_behaves_like 'get project download by strategy' it_behaves_like 'get project download by strategy'
context 'when rate limit is exceeded' do
let(:request) { get api(download_path, admin) }
include_examples 'when rate limit is exceeded'
end
end end
context 'when user is a maintainer' do context 'when user is a maintainer' do
...@@ -329,6 +348,12 @@ describe API::ProjectExport do ...@@ -329,6 +348,12 @@ describe API::ProjectExport do
let(:user) { admin } let(:user) { admin }
it_behaves_like 'post project export start' it_behaves_like 'post project export start'
context 'when rate limit is exceeded' do
let(:request) { post api(path, admin) }
include_examples 'when rate limit is exceeded'
end
end end
context 'when user is a maintainer' do context 'when user is a maintainer' do
......
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