Commit 1a3d1170 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 7797cff0 af1ff616
/* eslint-disable no-restricted-globals */
import { logger } from '@rails/actioncable';
// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.
// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
const now = () => new Date().getTime();
const secondsSince = (time) => (now() - time) / 1000;
class ConnectionMonitor {
constructor(connection) {
this.visibilityDidChange = this.visibilityDidChange.bind(this);
this.connection = connection;
this.reconnectAttempts = 0;
}
start() {
if (!this.isRunning()) {
this.startedAt = now();
delete this.stoppedAt;
this.startPolling();
addEventListener('visibilitychange', this.visibilityDidChange);
logger.log(
`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
);
}
}
stop() {
if (this.isRunning()) {
this.stoppedAt = now();
this.stopPolling();
removeEventListener('visibilitychange', this.visibilityDidChange);
logger.log('ConnectionMonitor stopped');
}
}
isRunning() {
return this.startedAt && !this.stoppedAt;
}
recordPing() {
this.pingedAt = now();
}
recordConnect() {
this.reconnectAttempts = 0;
this.recordPing();
delete this.disconnectedAt;
logger.log('ConnectionMonitor recorded connect');
}
recordDisconnect() {
this.disconnectedAt = now();
logger.log('ConnectionMonitor recorded disconnect');
}
// Private
startPolling() {
this.stopPolling();
this.poll();
}
stopPolling() {
clearTimeout(this.pollTimeout);
}
poll() {
this.pollTimeout = setTimeout(() => {
this.reconnectIfStale();
this.poll();
}, this.getPollInterval());
}
getPollInterval() {
const { staleThreshold, reconnectionBackoffRate } = this.constructor;
const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
const jitter = jitterMax * Math.random();
return staleThreshold * 1000 * backoff * (1 + jitter);
}
reconnectIfStale() {
if (this.connectionIsStale()) {
logger.log(
`ConnectionMonitor detected stale connection. reconnectAttempts = ${
this.reconnectAttempts
}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
this.constructor.staleThreshold
} s`,
);
this.reconnectAttempts += 1;
if (this.disconnectedRecently()) {
logger.log(
`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
this.disconnectedAt,
)} s`,
);
} else {
logger.log('ConnectionMonitor reopening');
this.connection.reopen();
}
}
}
get refreshedAt() {
return this.pingedAt ? this.pingedAt : this.startedAt;
}
connectionIsStale() {
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
}
disconnectedRecently() {
return (
this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
);
}
visibilityDidChange() {
if (document.visibilityState === 'visible') {
setTimeout(() => {
if (this.connectionIsStale() || !this.connection.isOpen()) {
logger.log(
`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
);
this.connection.reopen();
}
}, 200);
}
}
}
ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
ConnectionMonitor.reconnectionBackoffRate = 0.15;
export default ConnectionMonitor;
import { createConsumer } from '@rails/actioncable'; import { createConsumer } from '@rails/actioncable';
import ConnectionMonitor from './actioncable_connection_monitor';
export default createConsumer(); const consumer = createConsumer();
if (consumer.connection) {
consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
}
export default consumer;
...@@ -95,7 +95,12 @@ export default { ...@@ -95,7 +95,12 @@ export default {
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags"> <p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span> <span class="font-weight-bold">{{ __('Tags:') }}</span>
<span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ tag }}</span> <span
v-for="(tag, i) in job.tags"
:key="i"
class="badge badge-pill badge-primary gl-badge sm"
>{{ tag }}</span
>
</p> </p>
</div> </div>
</template> </template>
...@@ -23,17 +23,15 @@ function mountRemoveMemberModal() { ...@@ -23,17 +23,15 @@ function mountRemoveMemberModal() {
}); });
} }
document.addEventListener('DOMContentLoaded', () => { groupsSelect();
groupsSelect(); memberExpirationDate();
memberExpirationDate(); memberExpirationDate('.js-access-expiration-date-groups');
memberExpirationDate('.js-access-expiration-date-groups'); mountRemoveMemberModal();
mountRemoveMemberModal(); initInviteMembersModal();
initInviteMembersModal(); initInviteMembersTrigger();
initInviteMembersTrigger();
new Members(); // eslint-disable-line no-new new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
});
if (window.gon.features.vueProjectMembersList) { if (window.gon.features.vueProjectMembersList) {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions']; const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
......
...@@ -94,8 +94,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -94,8 +94,7 @@ class Projects::NotesController < Projects::ApplicationController
def create_rate_limit def create_rate_limit
key = :notes_create key = :notes_create
return unless rate_limiter.throttled?(key, scope: [current_user], users_allowlist: rate_limit_users_allowlist)
return unless rate_limiter.throttled?(key, scope: [current_user])
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
...@@ -104,4 +103,8 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -104,4 +103,8 @@ class Projects::NotesController < Projects::ApplicationController
def rate_limiter def rate_limiter
::Gitlab::ApplicationRateLimiter ::Gitlab::ApplicationRateLimiter
end end
def rate_limit_users_allowlist
Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
end
end end
...@@ -57,12 +57,18 @@ module Mutations ...@@ -57,12 +57,18 @@ module Mutations
end end
def verify_rate_limit!(current_user) def verify_rate_limit!(current_user)
rate_limiter, key = ::Gitlab::ApplicationRateLimiter, :notes_create return unless rate_limit_throttled?
return unless rate_limiter.throttled?(key, scope: [current_user])
raise Gitlab::Graphql::Errors::ResourceNotAvailable, raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'This endpoint has been requested too many times. Try again later.' 'This endpoint has been requested too many times. Try again later.'
end end
def rate_limit_throttled?
rate_limiter = ::Gitlab::ApplicationRateLimiter
allowlist = Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
rate_limiter.throttled?(:notes_create, scope: [current_user], users_allowlist: allowlist)
end
end end
end end
end end
......
...@@ -329,6 +329,7 @@ module ApplicationSettingsHelper ...@@ -329,6 +329,7 @@ module ApplicationSettingsHelper
:email_restrictions, :email_restrictions,
:issues_create_limit, :issues_create_limit,
:notes_create_limit, :notes_create_limit,
:notes_create_limit_allowlist_raw,
:raw_blob_request_limit, :raw_blob_request_limit,
:project_import_limit, :project_import_limit,
:project_export_limit, :project_export_limit,
......
...@@ -447,6 +447,10 @@ class ApplicationSetting < ApplicationRecord ...@@ -447,6 +447,10 @@ class ApplicationSetting < ApplicationRecord
validates :notes_create_limit, validates :notes_create_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :notes_create_limit_allowlist,
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
attr_encrypted :asset_proxy_secret_key, attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv, mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated, key: Settings.attr_encrypted_db_key_base_truncated,
......
...@@ -93,7 +93,6 @@ module ApplicationSettingImplementation ...@@ -93,7 +93,6 @@ module ApplicationSettingImplementation
import_sources: Settings.gitlab['import_sources'], import_sources: Settings.gitlab['import_sources'],
invisible_captcha_enabled: false, invisible_captcha_enabled: false,
issues_create_limit: 300, issues_create_limit: 300,
notes_create_limit: 300,
local_markdown_version: 0, local_markdown_version: 0,
login_recaptcha_protection_enabled: false, login_recaptcha_protection_enabled: false,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
...@@ -101,6 +100,8 @@ module ApplicationSettingImplementation ...@@ -101,6 +100,8 @@ module ApplicationSettingImplementation
max_import_size: 0, max_import_size: 0,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true, mirror_available: true,
notes_create_limit: 300,
notes_create_limit_allowlist: [],
notify_on_unknown_sign_in: true, notify_on_unknown_sign_in: true,
outbound_local_requests_whitelist: [], outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true, password_authentication_enabled_for_git: true,
...@@ -270,6 +271,14 @@ module ApplicationSettingImplementation ...@@ -270,6 +271,14 @@ module ApplicationSettingImplementation
self.protected_paths = strings_to_array(values) self.protected_paths = strings_to_array(values)
end end
def notes_create_limit_allowlist_raw
array_to_string(self.notes_create_limit_allowlist)
end
def notes_create_limit_allowlist_raw=(values)
self.notes_create_limit_allowlist = strings_to_array(values).map(&:downcase)
end
def asset_proxy_allowlist=(values) def asset_proxy_allowlist=(values)
values = strings_to_array(values) if values.is_a?(String) values = strings_to_array(values) if values.is_a?(String)
......
...@@ -5,5 +5,8 @@ ...@@ -5,5 +5,8 @@
.form-group .form-group
= f.label :notes_create_limit, _('Max requests per minute per user'), class: 'label-bold' = f.label :notes_create_limit, _('Max requests per minute per user'), class: 'label-bold'
= f.number_field :notes_create_limit, class: 'form-control gl-form-input' = f.number_field :notes_create_limit, class: 'form-control gl-form-input'
.form-group
= f.label :notes_create_limit_allowlist, _('List of users to be excluded from the limit'), class: 'label-bold'
= f.text_area :notes_create_limit_allowlist_raw, placeholder: 'username1, username2', class: 'form-control gl-form-input', rows: 5
= f.submit _('Save changes'), class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } = f.submit _('Save changes'), class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' }
---
title: Add an allowlist to exclude users from the rate limit on notes creation
merge_request: 53866
author:
type: added
---
title: Apply new GitLab UI for badge in job page sidebar
merge_request: 53386
author: Yogi (@yo)
type: other
# frozen_string_literal: true
class AddNotesCreateLimitAllowlistToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :notes_create_limit_allowlist, :text, array: true, default: [], null: false
end
end
e1bd58eeaf63caf473680a8c4b7269cc63e7c0d6e8d4e71636608e10c9731c85
\ No newline at end of file
...@@ -9414,6 +9414,7 @@ CREATE TABLE application_settings ( ...@@ -9414,6 +9414,7 @@ CREATE TABLE application_settings (
asset_proxy_allowlist text, asset_proxy_allowlist text,
keep_latest_artifact boolean DEFAULT true NOT NULL, keep_latest_artifact boolean DEFAULT true NOT NULL,
notes_create_limit integer DEFAULT 300 NOT NULL, notes_create_limit integer DEFAULT 300 NOT NULL,
notes_create_limit_allowlist text[] DEFAULT '{}'::text[] NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), 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_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
......
...@@ -10,8 +10,9 @@ type: reference ...@@ -10,8 +10,9 @@ type: reference
This is the GitLab Support Team's collection of information regarding the GitLab Rails This is the GitLab Support Team's collection of information regarding the GitLab Rails
console, for use while troubleshooting. It is listed here for transparency, console, for use while troubleshooting. It is listed here for transparency,
and it may be useful for users with experience with these tools. If you are currently and it may be useful for users with experience with these tools. If you are currently
having an issue with GitLab, it is highly recommended that you check your having an issue with GitLab, it is highly recommended that you first check
[support options](https://about.gitlab.com/support/) first, before attempting to use our guide on [navigating our Rails console](navigating_gitlab_via_rails_console.md),
and your [support options](https://about.gitlab.com/support/), before attempting to use
this information. this information.
WARNING: WARNING:
......
...@@ -194,10 +194,7 @@ To do this: ...@@ -194,10 +194,7 @@ To do this:
## Optional enforcement of Personal Access Token expiry **(ULTIMATE SELF)** ## Optional enforcement of Personal Access Token expiry **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab Ultimate 13.1. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab Ultimate 13.1.
> - It is deployed behind a feature flag, disabled by default. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/296881) in GitLab 13.9.
> - It is disabled on GitLab.com.
> - It is not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-optional-enforcement-of-personal-access-token-expiry-feature). **(FREE SELF)**
GitLab administrators can choose to prevent personal access tokens from expiring GitLab administrators can choose to prevent personal access tokens from expiring
automatically. The tokens are usable after the expiry date, unless they are revoked explicitly. automatically. The tokens are usable after the expiry date, unless they are revoked explicitly.
...@@ -208,23 +205,6 @@ To do this: ...@@ -208,23 +205,6 @@ To do this:
1. Expand the **Account and limit** section. 1. Expand the **Account and limit** section.
1. Uncheck the **Enforce personal access token expiration** checkbox. 1. Uncheck the **Enforce personal access token expiration** checkbox.
### Enable or disable optional enforcement of Personal Access Token expiry Feature **(FREE SELF)**
Optional Enforcement of Personal Access Token Expiry is deployed behind a feature flag and is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can enable it for your instance from the [rails console](../../../administration/feature_flags.md#start-the-gitlab-rails-console).
To enable it:
```ruby
Feature.enable(:enforce_pat_expiration)
```
To disable it:
```ruby
Feature.disable(:enforce_pat_expiration)
```
## Disabling user profile name changes **(PREMIUM SELF)** ## Disabling user profile name changes **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24605) in GitLab 12.7. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24605) in GitLab 12.7.
......
...@@ -47,8 +47,7 @@ module EE ...@@ -47,8 +47,7 @@ module EE
end end
def enforce_pat_expiration_feature_available? def enforce_pat_expiration_feature_available?
License.feature_available?(:enforce_pat_expiration) && License.feature_available?(:enforce_personal_access_token_expiration)
::Feature.enabled?(:enforce_pat_expiration, type: :licensed, default_enabled: false)
end end
end end
......
...@@ -144,7 +144,7 @@ class License < ApplicationRecord ...@@ -144,7 +144,7 @@ class License < ApplicationRecord
dast dast
dependency_scanning dependency_scanning
devops_adoption devops_adoption
enforce_pat_expiration enforce_personal_access_token_expiration
enforce_ssh_key_expiration enforce_ssh_key_expiration
enterprise_templates enterprise_templates
environment_alerts environment_alerts
......
---
title: Optional enforcement of PAT expiration (feature flag removed)
merge_request: 53660
author:
type: added
...@@ -12,7 +12,7 @@ RSpec.describe Profiles::PersonalAccessTokensController do ...@@ -12,7 +12,7 @@ RSpec.describe Profiles::PersonalAccessTokensController do
before do before do
sign_in(user) sign_in(user)
stub_licensed_features(enforce_pat_expiration: licensed) stub_licensed_features(enforce_personal_access_token_expiration: licensed)
stub_application_setting(enforce_pat_expiration: application_setting) stub_application_setting(enforce_pat_expiration: application_setting)
end end
......
...@@ -156,7 +156,7 @@ RSpec.describe PersonalAccessTokensHelper do ...@@ -156,7 +156,7 @@ RSpec.describe PersonalAccessTokensHelper do
describe '#enforce_pat_expiration_feature_available?' do describe '#enforce_pat_expiration_feature_available?' do
subject { helper.enforce_pat_expiration_feature_available? } subject { helper.enforce_pat_expiration_feature_available? }
let(:feature) { :enforce_pat_expiration } let(:feature) { :enforce_personal_access_token_expiration }
it_behaves_like 'feature availability' it_behaves_like 'feature availability'
end end
......
...@@ -220,7 +220,7 @@ RSpec.describe PersonalAccessToken do ...@@ -220,7 +220,7 @@ RSpec.describe PersonalAccessToken do
with_them do with_them do
before do before do
stub_licensed_features(enforce_pat_expiration: licensed) stub_licensed_features(enforce_personal_access_token_expiration: licensed)
stub_application_setting(enforce_pat_expiration: application_setting) stub_application_setting(enforce_pat_expiration: application_setting)
end end
...@@ -247,17 +247,14 @@ RSpec.describe PersonalAccessToken do ...@@ -247,17 +247,14 @@ RSpec.describe PersonalAccessToken do
subject { described_class.enforce_pat_expiration_feature_available? } subject { described_class.enforce_pat_expiration_feature_available? }
where(:feature_flag, :licensed, :result) do where(:licensed, :result) do
true | true | true true | true
true | false | false false | false
false | true | false
false | false | false
end end
with_them do with_them do
before do before do
stub_feature_flags(enforce_pat_expiration: feature_flag) stub_licensed_features(enforce_personal_access_token_expiration: licensed)
stub_licensed_features(enforce_pat_expiration: licensed)
end end
it { expect(subject).to be result } it { expect(subject).to be result }
......
...@@ -65,7 +65,7 @@ RSpec.describe PersonalAccessTokens::RevokeInvalidTokens do ...@@ -65,7 +65,7 @@ RSpec.describe PersonalAccessTokens::RevokeInvalidTokens do
with_them do with_them do
before do before do
stub_licensed_features(enforce_pat_expiration: licensed) stub_licensed_features(enforce_personal_access_token_expiration: licensed)
stub_application_setting(enforce_pat_expiration: application_setting) stub_application_setting(enforce_pat_expiration: application_setting)
it_behaves_like behavior it_behaves_like behavior
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
module API module API
module Helpers module Helpers
module RateLimiter module RateLimiter
def check_rate_limit!(key, scope) def check_rate_limit!(key, scope, users_allowlist = nil)
if rate_limiter.throttled?(key, scope: scope) if rate_limiter.throttled?(key, scope: scope, users_allowlist: users_allowlist)
log_request(key) log_request(key)
render_exceeded_limit_error! render_exceeded_limit_error!
end end
......
...@@ -73,7 +73,9 @@ module API ...@@ -73,7 +73,9 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note' optional :created_at, type: String, desc: 'The creation date of the note'
end end
post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do
check_rate_limit! :notes_create, [current_user] allowlist =
Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
check_rate_limit! :notes_create, [current_user], allowlist
noteable = find_noteable(noteable_type, params[:noteable_id]) noteable = find_noteable(noteable_type, params[:noteable_id])
opts = { opts = {
......
...@@ -47,15 +47,17 @@ module Gitlab ...@@ -47,15 +47,17 @@ module Gitlab
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project) # @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 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` # @option interval [Integer] Optional interval value to override default one registered in `.rate_limits`
# @option users_allowlist [Array<String>] Optional list of usernames to excepted from the limit. This param will only be functional if Scope includes a current user.
# #
# @return [Boolean] Whether or not a request should be throttled # @return [Boolean] Whether or not a request should be throttled
def throttled?(key, scope: nil, interval: nil, threshold: nil) def throttled?(key, **options)
return unless rate_limits[key] return unless rate_limits[key]
threshold_value = threshold || threshold(key) return if scoped_user_in_allowlist?(options)
threshold_value = options[:threshold] || threshold(key)
threshold_value > 0 && threshold_value > 0 &&
increment(key, scope, interval) > threshold_value increment(key, options[:scope], options[:interval]) > threshold_value
end end
# Increments the given cache key and increments the value by 1 with the # Increments the given cache key and increments the value by 1 with the
...@@ -141,6 +143,15 @@ module Gitlab ...@@ -141,6 +143,15 @@ module Gitlab
def application_settings def application_settings
Gitlab::CurrentSettings.current_application_settings Gitlab::CurrentSettings.current_application_settings
end end
def scoped_user_in_allowlist?(options)
return unless options[:users_allowlist].present?
scoped_user = [options[:scope]].flatten.find { |s| s.is_a?(User) }
return unless scoped_user
scoped_user.username.downcase.in?(options[:users_allowlist])
end
end end
end end
end end
...@@ -17664,6 +17664,9 @@ msgstr "" ...@@ -17664,6 +17664,9 @@ msgstr ""
msgid "List of all merge commits" msgid "List of all merge commits"
msgstr "" msgstr ""
msgid "List of users to be excluded from the limit"
msgstr ""
msgid "List options" msgid "List options"
msgstr "" msgstr ""
......
...@@ -730,11 +730,11 @@ RSpec.describe Projects::NotesController do ...@@ -730,11 +730,11 @@ RSpec.describe Projects::NotesController do
context 'when the endpoint receives requests above the limit' do context 'when the endpoint receives requests above the limit' do
before do before do
stub_application_setting(notes_create_limit: 5) stub_application_setting(notes_create_limit: 3)
end end
it 'prevents from creating more notes', :request_store do it 'prevents from creating more notes', :request_store do
5.times { create! } 3.times { create! }
expect { create! } expect { create! }
.to change { Gitlab::GitalyClient.get_request_count }.by(0) .to change { Gitlab::GitalyClient.get_request_count }.by(0)
...@@ -760,7 +760,16 @@ RSpec.describe Projects::NotesController do ...@@ -760,7 +760,16 @@ RSpec.describe Projects::NotesController do
project.add_developer(user) project.add_developer(user)
sign_in(user) sign_in(user)
6.times { create! } 4.times { create! }
end
it 'allows user in allow-list to create notes, even if the case is different' do
user.update_attribute(:username, user.username.titleize)
stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"])
3.times { create! }
create!
expect(response).to have_gitlab_http_status(:found)
end end
end end
end end
......
import ConnectionMonitor from '~/actioncable_connection_monitor';
describe('ConnectionMonitor', () => {
let monitor;
beforeEach(() => {
monitor = new ConnectionMonitor({});
});
describe('#getPollInterval', () => {
beforeEach(() => {
Math.originalRandom = Math.random;
});
afterEach(() => {
Math.random = Math.originalRandom;
});
const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor;
const backoffFactor = 1 + reconnectionBackoffRate;
const ms = 1000;
it('uses exponential backoff', () => {
Math.random = () => 0;
monitor.reconnectAttempts = 0;
expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
monitor.reconnectAttempts = 1;
expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms);
monitor.reconnectAttempts = 2;
expect(monitor.getPollInterval()).toEqual(
staleThreshold * backoffFactor * backoffFactor * ms,
);
});
it('caps exponential backoff after some number of reconnection attempts', () => {
Math.random = () => 0;
monitor.reconnectAttempts = 42;
const cappedPollInterval = monitor.getPollInterval();
monitor.reconnectAttempts = 9001;
expect(monitor.getPollInterval()).toEqual(cappedPollInterval);
});
it('uses 100% jitter when 0 reconnection attempts', () => {
Math.random = () => 0;
expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
Math.random = () => 0.5;
expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms);
});
it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => {
monitor.reconnectAttempts = 1;
Math.random = () => 0.25;
expect(monitor.getPollInterval()).toEqual(
staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms,
);
Math.random = () => 0.5;
expect(monitor.getPollInterval()).toEqual(
staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms,
);
});
it('applies jitter after capped exponential backoff', () => {
monitor.reconnectAttempts = 9001;
Math.random = () => 0;
const withoutJitter = monitor.getPollInterval();
Math.random = () => 0.5;
const withJitter = monitor.getPollInterval();
expect(withJitter).toBeGreaterThan(withoutJitter);
});
});
});
...@@ -120,6 +120,15 @@ RSpec.describe ApplicationSetting do ...@@ -120,6 +120,15 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(5.5).for(:notes_create_limit) } it { is_expected.not_to allow_value(5.5).for(:notes_create_limit) }
it { is_expected.not_to allow_value(-2).for(:notes_create_limit) } it { is_expected.not_to allow_value(-2).for(:notes_create_limit) }
def many_usernames(num = 100)
Array.new(num) { |i| "username#{i}" }
end
it { is_expected.to allow_value(many_usernames(100)).for(:notes_create_limit_allowlist) }
it { is_expected.not_to allow_value(many_usernames(101)).for(:notes_create_limit_allowlist) }
it { is_expected.not_to allow_value(nil).for(:notes_create_limit_allowlist) }
it { is_expected.to allow_value([]).for(:notes_create_limit_allowlist) }
context 'help_page_documentation_base_url validations' do context 'help_page_documentation_base_url validations' do
it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) } it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) }
it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) } it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) }
......
...@@ -74,4 +74,12 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro ...@@ -74,4 +74,12 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro
it_behaves_like 'a Note mutation that does not create a Note' it_behaves_like 'a Note mutation that does not create a Note'
it_behaves_like 'a mutation that returns top-level errors', it_behaves_like 'a mutation that returns top-level errors',
errors: ['This endpoint has been requested too many times. Try again later.'] errors: ['This endpoint has been requested too many times. Try again later.']
context 'when the user is in the allowlist' do
before do
stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"])
end
it_behaves_like 'a Note mutation that creates a Note'
end
end end
...@@ -127,6 +127,12 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| ...@@ -127,6 +127,12 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end end
describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
let(:params) { { body: 'hi!' } }
subject do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
end
it "creates a new note" do it "creates a new note" do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' } post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' }
...@@ -277,15 +283,25 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| ...@@ -277,15 +283,25 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
context 'when request exceeds the rate limit' do context 'when request exceeds the rate limit' do
before do before do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) stub_application_setting(notes_create_limit: 1)
allow(::Gitlab::ApplicationRateLimiter).to receive(:increment).and_return(2)
end end
it 'prevents users from creating more notes' do it 'prevents user from creating more notes' do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' } subject
expect(response).to have_gitlab_http_status(:too_many_requests) expect(response).to have_gitlab_http_status(:too_many_requests)
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.') expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end end
it 'allows user in allow-list to create notes' do
stub_application_setting(notes_create_limit_allowlist: ["#{user.username}"])
subject
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
end end
end end
......
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