summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-01-04 12:10:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-01-04 12:10:44 +0000
commit9d0b65f4b8aadf9eea57d40bc1627a8433699602 (patch)
tree1a327c2d99df613736106d3561524c5e2ede09ef
parentef863c1f85c474c13fd1bb9d84b00ad53f649ed0 (diff)
downloadgitlab-ce-9d0b65f4b8aadf9eea57d40bc1627a8433699602.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue5
-rw-r--r--app/assets/javascripts/notebook/cells/output/latex.vue45
-rw-r--r--app/assets/stylesheets/components/avatar.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb11
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/models/application_setting.rb4
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml7
-rw-r--r--changelogs/unreleased/allow-custom-rate-limiting-response.yml5
-rw-r--r--changelogs/unreleased/fix-identicon-dark-mode.yml5
-rw-r--r--changelogs/unreleased/jdb-juptyer-notebooks-latex-support.yml5
-rw-r--r--changelogs/unreleased/sh-remove-lfs-chunked-encoding-feature-flag.yml5
-rw-r--r--config/feature_flags/development/lfs_chunked_encoding.yml8
-rw-r--r--db/migrate/20201230161206_add_rate_limiting_response_text_to_application_settings.rb12
-rw-r--r--db/migrate/20210101110640_set_limit_for_rate_limiting_response_text.rb16
-rw-r--r--db/schema_migrations/202012301612061
-rw-r--r--db/schema_migrations/202101011106401
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/settings.md7
-rw-r--r--doc/install/README.md7
-rw-r--r--doc/install/requirements.md15
-rw-r--r--doc/user/admin_area/settings/user_and_ip_rate_limits.md11
-rw-r--r--lib/gitlab/rack_attack.rb13
-rw-r--r--lib/gitlab/throttle.rb6
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json1
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js10
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js42
-rw-r--r--spec/frontend/notebook/cells/output/latex_spec.js40
-rw-r--r--spec/lib/gitlab/rack_attack_spec.rb9
-rw-r--r--spec/lib/gitlab/throttle_spec.rb28
-rw-r--r--spec/requests/lfs_http_spec.rb16
-rw-r--r--spec/requests/rack_attack_global_spec.rb18
-rw-r--r--spec/support/helpers/rack_attack_spec_helpers.rb1
-rw-r--r--yarn.lock15
36 files changed, 324 insertions, 67 deletions
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 113d8cfc435..5f7ef4a4377 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -2,6 +2,7 @@
import CodeOutput from '../code/index.vue';
import HtmlOutput from './html.vue';
import ImageOutput from './image.vue';
+import LatexOutput from './latex.vue';
export default {
props: {
@@ -35,6 +36,8 @@ export default {
return 'image/jpeg';
} else if (output.data['text/html']) {
return 'text/html';
+ } else if (output.data['text/latex']) {
+ return 'text/latex';
} else if (output.data['image/svg+xml']) {
return 'image/svg+xml';
}
@@ -59,6 +62,8 @@ export default {
return ImageOutput;
} else if (output.data['text/html']) {
return HtmlOutput;
+ } else if (output.data['text/latex']) {
+ return LatexOutput;
} else if (output.data['image/svg+xml']) {
return HtmlOutput;
}
diff --git a/app/assets/javascripts/notebook/cells/output/latex.vue b/app/assets/javascripts/notebook/cells/output/latex.vue
new file mode 100644
index 00000000000..db9e61dce82
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/latex.vue
@@ -0,0 +1,45 @@
+<script>
+import 'mathjax/es5/tex-svg';
+import Prompt from '../prompt.vue';
+
+export default {
+ name: 'LatexOutput',
+ components: {
+ Prompt,
+ },
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ rawCode: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ code() {
+ // MathJax will not parse out the inline delimeters "$$" correctly
+ // so we remove them from the raw code itself
+ const parsedCode = this.rawCode.replace(/\$\$/g, '');
+ const svg = window.MathJax.tex2svg(parsedCode);
+
+ // NOTE: This is used with `v-html` and not `v-safe-html` due to an
+ // issue with dompurify stripping out xlink attributes from use tags
+ return svg.outerHTML;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="output">
+ <prompt type="Out" :count="count" :show-output="index === 0" />
+ <!-- eslint-disable -->
+ <div ref="maths" v-html="code"></div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 67213eedca8..3c8abe43070 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -68,7 +68,7 @@ $avatar-sizes: (
);
$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
- $identicon-orange, $gray-darker;
+ $identicon-orange, $identicon-gray;
%avatar-circle {
float: left;
@@ -125,8 +125,8 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
.identicon {
text-align: center;
vertical-align: top;
- color: $gray-700;
- background-color: $gray-darker;
+ color: $identicon-text-color;
+ background-color: $identicon-gray;
// Sizes
@each $size, $size-config in $avatar-sizes {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 808813599c5..5f0ba8ca9ce 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -629,12 +629,14 @@ $note-icon-gutter-width: 55px;
/*
* Identicon
*/
+$identicon-text-color: #525252 !default;
$identicon-red: #ffebee !default;
$identicon-purple: #f3e5f5 !default;
$identicon-indigo: #e8eaf6 !default;
$identicon-blue: #e3f2fd !default;
$identicon-teal: #e0f2f1 !default;
$identicon-orange: #fbe9e7 !default;
+$identicon-gray: #eee !default;
/*
* Calendar
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index 9b7cbcb2dfe..2de29da4b45 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -103,18 +103,13 @@ module Repositories
end
def upload_headers
- headers = {
+ {
Authorization: authorization_header,
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request.
- 'Content-Type': LFS_TRANSFER_CONTENT_TYPE
+ 'Content-Type': LFS_TRANSFER_CONTENT_TYPE,
+ 'Transfer-Encoding': 'chunked'
}
-
- if Feature.enabled?(:lfs_chunked_encoding, project, default_enabled: true)
- headers['Transfer-Encoding'] = 'chunked'
- end
-
- headers
end
def lfs_check_batch_operation!
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 7866e3e3d9f..cc91e2cf809 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -335,7 +335,8 @@ module ApplicationSettingsHelper
:group_export_limit,
:group_download_export_limit,
:wiki_page_max_content_bytes,
- :container_registry_delete_tags_service_timeout
+ :container_registry_delete_tags_service_timeout,
+ :rate_limiting_response_text
]
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 9b9db7f93fd..58bfc1948b4 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -400,6 +400,10 @@ class ApplicationSetting < ApplicationRecord
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
+ validates :rate_limiting_response_text,
+ length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
+ allow_blank: true
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 105889a364a..b68125d529a 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -172,7 +172,8 @@ module ApplicationSettingImplementation
container_registry_delete_tags_service_timeout: 250,
container_registry_expiration_policies_worker_capacity: 0,
kroki_enabled: false,
- kroki_url: nil
+ kroki_url: nil,
+ rate_limiting_response_text: nil
}
end
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index b06070d15d4..11ffe3f56e3 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -49,5 +49,12 @@
.form-group
= f.label :throttle_authenticated_web_period_in_seconds, 'Authenticated web rate limit period in seconds', class: 'label-bold'
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+ %hr
+ %h5
+ = _('Response text')
+ .form-group
+ = f.label :rate_limiting_response_text, class: 'label-bold' do
+ = _('A plain-text response to show to clients that hit the rate limit.')
+ = f.text_area :rate_limiting_response_text, placeholder: ::Gitlab::Throttle::DEFAULT_RATE_LIMITING_RESPONSE_TEXT, class: 'form-control', rows: 5
= f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' }
diff --git a/changelogs/unreleased/allow-custom-rate-limiting-response.yml b/changelogs/unreleased/allow-custom-rate-limiting-response.yml
new file mode 100644
index 00000000000..059e2ac8caf
--- /dev/null
+++ b/changelogs/unreleased/allow-custom-rate-limiting-response.yml
@@ -0,0 +1,5 @@
+---
+title: Allow custom response to be set when rate limits are exceeded
+merge_request: 50693
+author:
+type: added
diff --git a/changelogs/unreleased/fix-identicon-dark-mode.yml b/changelogs/unreleased/fix-identicon-dark-mode.yml
new file mode 100644
index 00000000000..98e2e59aa3b
--- /dev/null
+++ b/changelogs/unreleased/fix-identicon-dark-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Fix identicon text color in dark mode
+merge_request: 49785
+author: "@yo"
+type: fixed
diff --git a/changelogs/unreleased/jdb-juptyer-notebooks-latex-support.yml b/changelogs/unreleased/jdb-juptyer-notebooks-latex-support.yml
new file mode 100644
index 00000000000..0d505c0f33c
--- /dev/null
+++ b/changelogs/unreleased/jdb-juptyer-notebooks-latex-support.yml
@@ -0,0 +1,5 @@
+---
+title: Add LaTeX support for Jupyter Notebooks
+merge_request: 49497
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-remove-lfs-chunked-encoding-feature-flag.yml b/changelogs/unreleased/sh-remove-lfs-chunked-encoding-feature-flag.yml
new file mode 100644
index 00000000000..f39b7061925
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-lfs-chunked-encoding-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove lfs_chunked_encoding feature flag
+merge_request: 50557
+author:
+type: changed
diff --git a/config/feature_flags/development/lfs_chunked_encoding.yml b/config/feature_flags/development/lfs_chunked_encoding.yml
deleted file mode 100644
index 92f534d1000..00000000000
--- a/config/feature_flags/development/lfs_chunked_encoding.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: lfs_chunked_encoding
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48269
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285581
-milestone: '13.6'
-type: development
-group:
-default_enabled: true
diff --git a/db/migrate/20201230161206_add_rate_limiting_response_text_to_application_settings.rb b/db/migrate/20201230161206_add_rate_limiting_response_text_to_application_settings.rb
new file mode 100644
index 00000000000..647455f5f88
--- /dev/null
+++ b/db/migrate/20201230161206_add_rate_limiting_response_text_to_application_settings.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddRateLimitingResponseTextToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in 20210101110640_set_limit_for_rate_limiting_response_text
+ def change
+ add_column :application_settings, :rate_limiting_response_text, :text
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20210101110640_set_limit_for_rate_limiting_response_text.rb b/db/migrate/20210101110640_set_limit_for_rate_limiting_response_text.rb
new file mode 100644
index 00000000000..b72f2ae7d70
--- /dev/null
+++ b/db/migrate/20210101110640_set_limit_for_rate_limiting_response_text.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class SetLimitForRateLimitingResponseText < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :application_settings, :rate_limiting_response_text, 255
+ end
+
+ def down
+ remove_text_limit :application_settings, :rate_limiting_response_text
+ end
+end
diff --git a/db/schema_migrations/20201230161206 b/db/schema_migrations/20201230161206
new file mode 100644
index 00000000000..594ca5ea890
--- /dev/null
+++ b/db/schema_migrations/20201230161206
@@ -0,0 +1 @@
+b3fcc73c6b61469d770e9eb9a14c88bb86398db4ab4b6dc5283718a147db0ac0 \ No newline at end of file
diff --git a/db/schema_migrations/20210101110640 b/db/schema_migrations/20210101110640
new file mode 100644
index 00000000000..e21e6768e7b
--- /dev/null
+++ b/db/schema_migrations/20210101110640
@@ -0,0 +1 @@
+8aac4108b658a7a0646ec230dc2568cb51fea0535b13dfba8b8c9e6edb401d07 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a9022190aeb..3693d6c0ea4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9376,12 +9376,14 @@ CREATE TABLE application_settings (
cloud_license_enabled boolean DEFAULT false NOT NULL,
disable_feed_token boolean DEFAULT false NOT NULL,
personal_access_token_prefix text,
+ rate_limiting_response_text text,
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)),
CONSTRAINT check_718b4458ae CHECK ((char_length(personal_access_token_prefix) <= 20)),
+ CONSTRAINT check_7227fad848 CHECK ((char_length(rate_limiting_response_text) <= 255)),
CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)),
CONSTRAINT check_9a719834eb CHECK ((char_length(secret_detection_token_revocation_url) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 5680687e87e..a71f49e724e 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -83,7 +83,8 @@ Example response:
"raw_blob_request_limit": 300,
"wiki_page_max_content_bytes": 52428800,
"require_admin_approval_after_user_signup": false,
- "personal_access_token_prefix": "GL-"
+ "personal_access_token_prefix": "GL-",
+ "rate_limiting_response_text": null
}
```
@@ -176,7 +177,8 @@ Example response:
"raw_blob_request_limit": 300,
"wiki_page_max_content_bytes": 52428800,
"require_admin_approval_after_user_signup": false,
- "personal_access_token_prefix": "GL-"
+ "personal_access_token_prefix": "GL-",
+ "rate_limiting_response_text": null
}
```
@@ -330,6 +332,7 @@ listed in the descriptions of the relevant settings.
| `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory.
| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events are created. [Bulk push events are created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services fire or not. Webhooks and services aren't submitted if it surpasses that value. |
+| `rate_limiting_response_text` | string | no | When rate limiting is enabled via the `throttle_*` settings, send this plain text response when a rate limit is exceeded. 'Retry later' is sent if this is blank. |
| `raw_blob_request_limit` | integer | no | Max number of requests per minute for each raw path. Default: 300. To disable throttling set to 0.|
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. |
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. |
diff --git a/doc/install/README.md b/doc/install/README.md
index f0aee9b6927..7b037da4c4a 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -79,9 +79,10 @@ package.
## Installing GitLab from source
-If the Omnibus GitLab package is not available in your distribution, you can
-install GitLab from source: Useful for unsupported systems like \*BSD. For an
-overview of the directory structure, read the [structure documentation](installation.md#gitlab-directory-structure).
+If the Omnibus GitLab package isn't available for your distribution, you can
+install GitLab from source. This can be useful with unsupported systems, like
+\*BSD. For an overview of the directory structure, see the
+[structure documentation](installation.md#gitlab-directory-structure).
[**> Install GitLab from source.**](installation.md)
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index cced6d0a3f6..69983edc383 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -16,7 +16,7 @@ as the hardware requirements that are needed to install and use GitLab.
- Ubuntu (16.04/18.04/20.04)
- Debian (9/10)
-- CentOS (6/7/8)
+- CentOS (7/8)
- openSUSE (Leap 15.1/Enterprise Server 12.2)
- Red Hat Enterprise Linux (please use the CentOS packages and instructions)
- Scientific Linux (please use the CentOS packages and instructions)
@@ -35,6 +35,9 @@ For the installation options, see [the main installation page](README.md).
Installation of GitLab on these operating systems is possible, but not supported.
Please see the [installation from source guide](installation.md) and the [installation guides](https://about.gitlab.com/install/) for more information.
+Please see [OS versions that are no longer supported](https://docs.gitlab.com/omnibus/package-information/deprecated_os.html) for Omnibus installs page
+for a list of supported and unsupported OS versions as well as the last support GitLab version for that OS.
+
### Microsoft Windows
GitLab is developed for Linux-based operating systems.
@@ -163,11 +166,11 @@ Support for [PostgreSQL 9.6 and 10 has been removed in GitLab 13.0](https://abou
#### Additional requirements for GitLab Geo
-If you're using [GitLab Geo](../administration/geo/index.md):
-
-- We strongly recommend running Omnibus-managed instances as they are actively
- developed and tested. We aim to be compatible with most external (not managed
- by Omnibus) databases (for example, [AWS Relational Database Service (RDS)](https://aws.amazon.com/rds/)) but we don't guarantee compatibility.
+If you're using [GitLab Geo](../administration/geo/index.md), we strongly
+recommend running Omnibus GitLab-managed instances, as we actively develop and
+test based on those. We try to be compatible with most external (not managed by
+Omnibus GitLab) databases (for example, [AWS Relational Database Service (RDS)](https://aws.amazon.com/rds/)),
+but we can't guarantee compatibility.
## Puma settings
diff --git a/doc/user/admin_area/settings/user_and_ip_rate_limits.md b/doc/user/admin_area/settings/user_and_ip_rate_limits.md
index 3289a201890..72c05d1115d 100644
--- a/doc/user/admin_area/settings/user_and_ip_rate_limits.md
+++ b/doc/user/admin_area/settings/user_and_ip_rate_limits.md
@@ -25,6 +25,17 @@ By default, all Git operations are first tried unathenticated. Because of this,
![user-and-ip-rate-limits](img/user_and_ip_rate_limits.png)
+## Response text
+
+A request that exceeds a rate limit will get a 429 response code and a
+plain-text body, which by default is:
+
+```plaintext
+Retry later
+```
+
+It is possible to customize this response text in the admin area.
+
## Use an HTTP header to bypass rate limiting
> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/622) in GitLab 13.6.
diff --git a/lib/gitlab/rack_attack.rb b/lib/gitlab/rack_attack.rb
index 7c336153e32..b136023d5f2 100644
--- a/lib/gitlab/rack_attack.rb
+++ b/lib/gitlab/rack_attack.rb
@@ -10,8 +10,17 @@ module Gitlab
def self.configure(rack_attack)
# This adds some methods used by our throttles to the `Rack::Request`
rack_attack::Request.include(Gitlab::RackAttack::Request)
- # Send the Retry-After header so clients (e.g. python-gitlab) can make good choices about delays
- Rack::Attack.throttled_response_retry_after_header = true
+
+ # This is Rack::Attack::DEFAULT_THROTTLED_RESPONSE, modified to allow a custom response
+ Rack::Attack.throttled_response = lambda do |env|
+ # Send the Retry-After header so clients (e.g. python-gitlab) can make good choices about delays
+ match_data = env['rack.attack.match_data']
+ now = match_data[:epoch_time]
+ retry_after = match_data[:period] - (now % match_data[:period])
+
+ [429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, [Gitlab::Throttle.rate_limiting_response_text]]
+ end
+
# Configure the throttles
configure_throttles(rack_attack)
diff --git a/lib/gitlab/throttle.rb b/lib/gitlab/throttle.rb
index aebf8d92cb3..520075012e8 100644
--- a/lib/gitlab/throttle.rb
+++ b/lib/gitlab/throttle.rb
@@ -2,6 +2,8 @@
module Gitlab
class Throttle
+ DEFAULT_RATE_LIMITING_RESPONSE_TEXT = 'Retry later'
+
def self.settings
Gitlab::CurrentSettings.current_application_settings
end
@@ -46,5 +48,9 @@ module Gitlab
{ limit: limit_proc, period: period_proc }
end
+
+ def self.rate_limiting_response_text
+ (settings.rate_limiting_response_text.presence || DEFAULT_RATE_LIMITING_RESPONSE_TEXT) + "\n"
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 88d9f3f3cbf..4de0b7732d2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1304,6 +1304,9 @@ msgstr ""
msgid "A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features"
msgstr ""
+msgid "A plain-text response to show to clients that hit the rate limit."
+msgstr ""
+
msgid "A platform value can be web, mob or app."
msgstr ""
@@ -23928,6 +23931,9 @@ msgstr ""
msgid "Response metrics (NGINX)"
msgstr ""
+msgid "Response text"
+msgstr ""
+
msgid "Restart Terminal"
msgstr ""
diff --git a/package.json b/package.json
index 741ca7b5285..b22a49071df 100644
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
"katex": "^0.10.0",
"lodash": "^4.17.20",
"marked": "^0.3.12",
+ "mathjax": "3",
"mermaid": "^8.5.2",
"mersenne-twister": "1.1.0",
"minimatch": "^3.0.4",
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 77d83b102aa..c715d779986 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -1,6 +1,9 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
import createDiffsStore from '~/diffs/store/modules';
import createNotesStore from '~/notes/stores/modules';
import diffFileMockDataReadable from '../mock_data/diff_file';
@@ -118,14 +121,17 @@ const changeViewerType = (store, newType, index = 0) =>
describe('DiffFile', () => {
let wrapper;
let store;
+ let axiosMock;
beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
({ wrapper, store } = createComponent({ file: getReadableFile() }));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ axiosMock.restore();
});
describe('bus events', () => {
@@ -353,8 +359,10 @@ describe('DiffFile', () => {
describe('loading', () => {
it('should have loading icon while loading a collapsed diffs', async () => {
+ const { load_collapsed_diff_url } = store.state.diffs.diffFiles[0];
+ axiosMock.onGet(load_collapsed_diff_url).reply(httpStatus.OK, getReadableFile());
makeFileAutomaticallyCollapsed(store);
- wrapper.vm.isLoadingCollapsedDiff = true;
+ wrapper.vm.requestDiff();
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index d12f57e80c7..2985abf0f4f 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -18,12 +18,14 @@ describe('Output component', () => {
};
beforeEach(() => {
+ // This is the output after rendering a jupyter notebook
json = getJSONFixture('blob/notebook/basic.json');
});
describe('text output', () => {
beforeEach((done) => {
- createComponent(json.cells[2].outputs[0]);
+ const textType = json.cells[2];
+ createComponent(textType.outputs[0]);
setImmediate(() => {
done();
@@ -41,7 +43,8 @@ describe('Output component', () => {
describe('image output', () => {
beforeEach((done) => {
- createComponent(json.cells[3].outputs[0]);
+ const imageType = json.cells[3];
+ createComponent(imageType.outputs[0]);
setImmediate(() => {
done();
@@ -55,23 +58,42 @@ describe('Output component', () => {
describe('html output', () => {
it('renders raw HTML', () => {
- createComponent(json.cells[4].outputs[0]);
+ const htmlType = json.cells[4];
+ createComponent(htmlType.outputs[0]);
expect(vm.$el.querySelector('p')).not.toBeNull();
- expect(vm.$el.querySelectorAll('p').length).toBe(1);
+ expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
expect(vm.$el.textContent.trim()).toContain('test');
});
it('renders multiple raw HTML outputs', () => {
- createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]);
+ const htmlType = json.cells[4];
+ createComponent([htmlType.outputs[0], htmlType.outputs[0]]);
- expect(vm.$el.querySelectorAll('p').length).toBe(2);
+ expect(vm.$el.querySelectorAll('p')).toHaveLength(2);
+ });
+ });
+
+ describe('LaTeX output', () => {
+ it('renders LaTeX', () => {
+ const output = {
+ data: {
+ 'text/latex': ['$$F(k) = \\int_{-\\infty}^{\\infty} f(x) e^{2\\pi i k} dx$$'],
+ 'text/plain': ['<IPython.core.display.Latex object>'],
+ },
+ metadata: {},
+ output_type: 'display_data',
+ };
+ createComponent(output);
+
+ expect(vm.$el.querySelector('.MathJax')).not.toBeNull();
});
});
describe('svg output', () => {
beforeEach((done) => {
- createComponent(json.cells[5].outputs[0]);
+ const svgType = json.cells[5];
+ createComponent(svgType.outputs[0]);
setImmediate(() => {
done();
@@ -85,7 +107,8 @@ describe('Output component', () => {
describe('default to plain text', () => {
beforeEach((done) => {
- createComponent(json.cells[6].outputs[0]);
+ const unknownType = json.cells[6];
+ createComponent(unknownType.outputs[0]);
setImmediate(() => {
done();
@@ -102,7 +125,8 @@ describe('Output component', () => {
});
it("renders as plain text when doesn't recognise other types", (done) => {
- createComponent(json.cells[7].outputs[0]);
+ const unknownType = json.cells[7];
+ createComponent(unknownType.outputs[0]);
setImmediate(() => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
diff --git a/spec/frontend/notebook/cells/output/latex_spec.js b/spec/frontend/notebook/cells/output/latex_spec.js
new file mode 100644
index 00000000000..848d2069421
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/latex_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount } from '@vue/test-utils';
+import LatexOutput from '~/notebook/cells/output/latex.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
+
+describe('LaTeX output cell', () => {
+ beforeEach(() => {
+ window.MathJax = {
+ tex2svg: jest.fn((code) => ({ outerHTML: code })),
+ };
+ });
+
+ const inlineLatex = '$$F(k) = \\int_{-\\infty}^{\\infty} f(x) e^{2\\pi i k} dx$$';
+ const count = 12345;
+
+ const createComponent = (rawCode, index) =>
+ shallowMount(LatexOutput, {
+ propsData: {
+ count,
+ index,
+ rawCode,
+ },
+ });
+
+ it.each`
+ index | expectation
+ ${0} | ${true}
+ ${1} | ${false}
+ `('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => {
+ const wrapper = createComponent(inlineLatex, index);
+ const prompt = wrapper.find(Prompt);
+
+ expect(prompt.props().count).toEqual(count);
+ expect(prompt.props().showOutput).toEqual(expectation);
+ });
+
+ it('strips the `$$` delimter from LaTeX', () => {
+ createComponent(inlineLatex, 0);
+ expect(window.MathJax.tex2svg).toHaveBeenCalledWith(expect.not.stringContaining('$$'));
+ });
+});
diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb
index d72863b0103..8fbf7494b64 100644
--- a/spec/lib/gitlab/rack_attack_spec.rb
+++ b/spec/lib/gitlab/rack_attack_spec.rb
@@ -22,8 +22,7 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do
stub_const("Rack::Attack", fake_rack_attack)
stub_const("Rack::Attack::Request", fake_rack_attack_request)
- # Expect rather than just allow, because this is actually fairly important functionality
- expect(fake_rack_attack).to receive(:throttled_response_retry_after_header=).with(true)
+ allow(fake_rack_attack).to receive(:throttled_response=)
allow(fake_rack_attack).to receive(:throttle)
allow(fake_rack_attack).to receive(:track)
allow(fake_rack_attack).to receive(:safelist)
@@ -36,6 +35,12 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do
expect(fake_rack_attack_request).to include(described_class::Request)
end
+ it 'configures the throttle response' do
+ described_class.configure(fake_rack_attack)
+
+ expect(fake_rack_attack).to have_received(:throttled_response=).with(an_instance_of(Proc))
+ end
+
it 'configures the safelist' do
described_class.configure(fake_rack_attack)
diff --git a/spec/lib/gitlab/throttle_spec.rb b/spec/lib/gitlab/throttle_spec.rb
index 7462b2e1c38..50d723193ac 100644
--- a/spec/lib/gitlab/throttle_spec.rb
+++ b/spec/lib/gitlab/throttle_spec.rb
@@ -30,4 +30,32 @@ RSpec.describe Gitlab::Throttle do
end
end
end
+
+ describe '.rate_limiting_response_text' do
+ subject { described_class.rate_limiting_response_text }
+
+ context 'when the setting is not present' do
+ before do
+ stub_application_setting(rate_limiting_response_text: '')
+ end
+
+ it 'returns the default value with a trailing newline' do
+ expect(subject).to eq(described_class::DEFAULT_RATE_LIMITING_RESPONSE_TEXT + "\n")
+ end
+ end
+
+ context 'when the setting is present' do
+ let(:response_text) do
+ 'Rate limit exceeded; see https://docs.gitlab.com/ee/user/gitlab_com/#gitlabcom-specific-rate-limits for more details'
+ end
+
+ before do
+ stub_application_setting(rate_limiting_response_text: response_text)
+ end
+
+ it 'returns the default value with a trailing newline' do
+ expect(subject).to eq(response_text + "\n")
+ end
+ end
+ end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 535d511a459..aeec18aee2b 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -193,10 +193,8 @@ RSpec.describe 'Git LFS API and storage' do
subject(:request) { post_lfs_json batch_url(project), body, headers }
let(:response) { request && super() }
- let(:lfs_chunked_encoding) { true }
before do
- stub_feature_flags(lfs_chunked_encoding: lfs_chunked_encoding)
project.lfs_objects << lfs_object
end
@@ -480,20 +478,6 @@ RSpec.describe 'Git LFS API and storage' do
expect(headers['Transfer-Encoding']).to eq('chunked')
end
- context 'when lfs_chunked_encoding feature is disabled' do
- let(:lfs_chunked_encoding) { false }
-
- it 'responds with upload hypermedia link' do
- expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first).to include(sample_object)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
-
- headers = json_response['objects'].first['actions']['upload']['header']
- expect(headers['Content-Type']).to eq('application/octet-stream')
- expect(headers['Transfer-Encoding']).to be_nil
- end
- end
-
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index c2e68df2c40..157fcac56fa 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -60,6 +60,24 @@ RSpec.describe 'Rack Attack global throttles' do
expect_rejection { get url_that_does_not_require_authentication }
end
+ context 'with custom response text' do
+ before do
+ stub_application_setting(rate_limiting_response_text: 'Custom response')
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ # the last straw
+ expect_rejection { get url_that_does_not_require_authentication }
+ expect(response.body).to eq("Custom response\n")
+ end
+ end
+
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
get url_that_does_not_require_authentication
diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb
index a8ae69885d8..d479420e87b 100644
--- a/spec/support/helpers/rack_attack_spec_helpers.rb
+++ b/spec/support/helpers/rack_attack_spec_helpers.rb
@@ -25,6 +25,7 @@ module RackAttackSpecHelpers
yield
expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response).to have_header('Retry-After')
end
def expect_ok(&block)
diff --git a/yarn.lock b/yarn.lock
index 4b70e94b7f9..30fcba5be37 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3057,7 +3057,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
-commander@2, commander@^2.10.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0:
+commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -7476,11 +7476,11 @@ karma@^4.2.0:
useragent "2.3.0"
katex@^0.10.0:
- version "0.10.0"
- resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.0.tgz#da562e5d0d5cc3aa602e27af8a9b8710bfbce765"
- integrity sha512-/WRvx+L1eVBrLwX7QzKU1dQuaGnE7E8hDvx3VWfZh9HbMiCfsKWJNnYZ0S8ZMDAfAyDSofdyXIrH/hujF1fYXg==
+ version "0.10.2"
+ resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.2.tgz#39973edbb65eda5b6f9e7f41648781e557dd4932"
+ integrity sha512-cQOmyIRoMloCoSIOZ1+gEwsksdJZ1EW4SWm3QzxSza/QsnZr6D4U1V9S4q+B/OLm2OQ8TCBecQ8MaIfnScI7cw==
dependencies:
- commander "^2.16.0"
+ commander "^2.19.0"
keyv@^3.0.0:
version "3.1.0"
@@ -8054,6 +8054,11 @@ marked@^0.3.12, marked@~0.3.6:
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
+mathjax@3:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.1.2.tgz#95c0d45ce2330ef7b6a815cebe7d61ecc26bbabd"
+ integrity sha512-BojKspBv4nNWzO1wC6VEI+g9gHDOhkaGHGgLxXkasdU4pwjdO5AXD5M/wcLPkXYPjZ/N+6sU8rjQTlyvN2cWiQ==
+
mathml-tag-names@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"