2019-04-11 08:17:24 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2022-01-11 01:10:58 -05:00
|
|
|
RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state do
|
2019-04-21 06:03:26 -04:00
|
|
|
include StubRequests
|
|
|
|
|
2022-06-07 14:09:27 -04:00
|
|
|
let(:ellipsis) { '…' }
|
2021-05-14 05:10:24 -04:00
|
|
|
let_it_be(:project) { create(:project) }
|
|
|
|
let_it_be_with_reload(:project_hook) { create(:project_hook, project: project) }
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
let(:data) do
|
|
|
|
{ before: 'oldrev', after: 'newrev', ref: 'ref' }
|
|
|
|
end
|
2020-08-10 23:11:00 -04:00
|
|
|
|
2017-09-07 07:29:12 -04:00
|
|
|
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
|
2017-04-27 06:08:57 -04:00
|
|
|
|
2018-03-13 18:38:25 -04:00
|
|
|
describe '#initialize' do
|
2019-07-26 09:03:06 -04:00
|
|
|
before do
|
|
|
|
stub_application_setting(setting_name => setting)
|
|
|
|
end
|
|
|
|
|
2019-07-30 06:53:23 -04:00
|
|
|
shared_examples_for 'respects outbound network setting' do
|
|
|
|
context 'when local requests are allowed' do
|
2019-07-26 09:03:06 -04:00
|
|
|
let(:setting) { true }
|
2019-07-26 06:21:52 -04:00
|
|
|
|
2019-07-26 09:03:06 -04:00
|
|
|
it { expect(hook.request_options[:allow_local_requests]).to be_truthy }
|
2019-07-26 06:21:52 -04:00
|
|
|
end
|
|
|
|
|
2019-07-30 06:53:23 -04:00
|
|
|
context 'when local requests are not allowed' do
|
2019-07-26 09:03:06 -04:00
|
|
|
let(:setting) { false }
|
2019-07-26 06:21:52 -04:00
|
|
|
|
2019-07-26 09:03:06 -04:00
|
|
|
it { expect(hook.request_options[:allow_local_requests]).to be_falsey }
|
2019-07-26 06:21:52 -04:00
|
|
|
end
|
2019-07-26 09:03:06 -04:00
|
|
|
end
|
2019-07-26 06:21:52 -04:00
|
|
|
|
2019-07-26 09:03:06 -04:00
|
|
|
context 'when SystemHook' do
|
|
|
|
let(:setting_name) { :allow_local_requests_from_system_hooks }
|
|
|
|
let(:hook) { described_class.new(build(:system_hook), data, :system_hook) }
|
2019-07-26 06:21:52 -04:00
|
|
|
|
2019-07-30 06:53:23 -04:00
|
|
|
include_examples 'respects outbound network setting'
|
2019-07-26 09:03:06 -04:00
|
|
|
end
|
2019-07-26 06:21:52 -04:00
|
|
|
|
2019-07-26 09:03:06 -04:00
|
|
|
context 'when ProjectHook' do
|
|
|
|
let(:setting_name) { :allow_local_requests_from_web_hooks_and_services }
|
|
|
|
let(:hook) { described_class.new(build(:project_hook), data, :project_hook) }
|
2018-03-13 18:38:25 -04:00
|
|
|
|
2019-07-30 06:53:23 -04:00
|
|
|
include_examples 'respects outbound network setting'
|
2018-03-13 18:38:25 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-02-16 01:12:24 -05:00
|
|
|
describe '#disabled?' do
|
|
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
|
|
|
|
subject { described_class.new(hook, data, :push_hooks, force: forced) }
|
|
|
|
|
|
|
|
let(:hook) { double(executable?: executable, allow_local_requests?: false) }
|
|
|
|
|
|
|
|
where(:forced, :executable, :disabled) do
|
|
|
|
false | true | false
|
|
|
|
false | false | true
|
|
|
|
true | true | false
|
|
|
|
true | false | false
|
|
|
|
end
|
|
|
|
|
|
|
|
with_them do
|
|
|
|
it { is_expected.to have_attributes(disabled?: disabled) }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
describe '#execute' do
|
2022-01-11 01:10:58 -05:00
|
|
|
let!(:uuid) { SecureRandom.uuid }
|
|
|
|
let(:headers) do
|
|
|
|
{
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
|
|
|
|
'X-Gitlab-Event' => 'Push Hook',
|
|
|
|
'X-Gitlab-Event-UUID' => uuid
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
# Set a stable value for the `X-Gitlab-Event-UUID` header.
|
|
|
|
Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
|
|
|
|
end
|
|
|
|
|
2022-07-15 14:09:50 -04:00
|
|
|
context 'when there is an interpolation error' do
|
|
|
|
let(:error) { ::WebHook::InterpolationError.new('boom') }
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
allow(project_hook).to receive(:interpolated_url).and_raise(error)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'logs the error' do
|
|
|
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error)
|
|
|
|
|
|
|
|
expect(service_instance).to receive(:log_execution).with(
|
|
|
|
execution_duration: (be > 0),
|
|
|
|
response: have_attributes(code: 200)
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when there are URL variables' do
|
|
|
|
before do
|
|
|
|
project_hook.update!(
|
|
|
|
url: 'http://example.com/{one}/{two}',
|
|
|
|
url_variables: { 'one' => 'a', 'two' => 'b' }
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'POSTs to the interpolated URL, and logs the hook.url' do
|
|
|
|
stub_full_request(project_hook.interpolated_url, method: :post)
|
|
|
|
|
|
|
|
expect(service_instance).to receive(:queue_log_execution_with_retry).with(
|
|
|
|
include(url: project_hook.url),
|
|
|
|
:ok
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
|
|
|
|
expect(WebMock)
|
|
|
|
.to have_requested(:post, stubbed_hostname(project_hook.interpolated_url)).once
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'there is userinfo' do
|
|
|
|
before do
|
|
|
|
project_hook.update!(url: 'http://{one}:{two}@example.com')
|
|
|
|
stub_full_request('http://example.com', method: :post)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'POSTs to the interpolated URL, and logs the hook.url' do
|
|
|
|
expect(service_instance).to receive(:queue_log_execution_with_retry).with(
|
|
|
|
include(url: project_hook.url),
|
|
|
|
:ok
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
|
|
|
|
expect(WebMock)
|
|
|
|
.to have_requested(:post, stubbed_hostname('http://example.com'))
|
|
|
|
.with(headers: headers.merge('Authorization' => 'Basic YTpi'))
|
|
|
|
.once
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
context 'when token is defined' do
|
2022-07-15 14:09:50 -04:00
|
|
|
before do
|
|
|
|
project_hook.token = generate(:token)
|
|
|
|
end
|
2017-04-27 06:08:57 -04:00
|
|
|
|
|
|
|
it 'POSTs to the webhook URL' do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
service_instance.execute
|
2019-08-27 05:04:27 -04:00
|
|
|
|
|
|
|
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).with(
|
2017-04-27 06:08:57 -04:00
|
|
|
headers: headers.merge({ 'X-Gitlab-Token' => project_hook.token })
|
|
|
|
).once
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'POSTs the data as JSON' do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
service_instance.execute
|
2019-08-27 05:04:27 -04:00
|
|
|
|
|
|
|
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).with(
|
2017-04-27 06:08:57 -04:00
|
|
|
headers: headers
|
|
|
|
).once
|
|
|
|
end
|
|
|
|
|
2022-03-22 05:07:15 -04:00
|
|
|
context 'when the data is a Gitlab::DataBuilder::Pipeline' do
|
|
|
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
let(:data) { ::Gitlab::DataBuilder::Pipeline.new(pipeline) }
|
|
|
|
|
|
|
|
it 'can log the request payload' do
|
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
|
|
|
|
# we call this with force to ensure that the logs are written inline,
|
|
|
|
# which tests that we can serialize the data to the DB correctly.
|
|
|
|
service = described_class.new(project_hook, data, :push_hooks, force: true)
|
|
|
|
|
|
|
|
expect { service.execute }.to change(::WebHookLog, :count).by(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-06-25 10:44:29 -04:00
|
|
|
context 'when auth credentials are present' do
|
2022-08-02 23:09:00 -04:00
|
|
|
let_it_be(:url) { 'https://example.org' }
|
2021-05-14 05:10:24 -04:00
|
|
|
let_it_be(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
|
2018-06-25 10:44:29 -04:00
|
|
|
|
|
|
|
it 'uses the credentials' do
|
2019-04-21 06:03:26 -04:00
|
|
|
stub_full_request(url, method: :post)
|
2018-06-25 10:44:29 -04:00
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
|
2019-04-21 06:03:26 -04:00
|
|
|
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
|
2018-06-25 10:44:29 -04:00
|
|
|
headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
|
|
|
|
).once
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when auth credentials are partial present' do
|
2022-08-02 23:09:00 -04:00
|
|
|
let_it_be(:url) { 'https://example.org' }
|
2021-05-14 05:10:24 -04:00
|
|
|
let_it_be(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
|
2018-06-25 10:44:29 -04:00
|
|
|
|
|
|
|
it 'uses the credentials anyways' do
|
2019-04-21 06:03:26 -04:00
|
|
|
stub_full_request(url, method: :post)
|
2018-06-25 10:44:29 -04:00
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
|
2019-04-21 06:03:26 -04:00
|
|
|
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
|
2018-06-25 10:44:29 -04:00
|
|
|
headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
|
|
|
|
).once
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
it 'catches exceptions' do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_raise(StandardError.new('Some error'))
|
2017-04-27 06:08:57 -04:00
|
|
|
|
|
|
|
expect { service_instance.execute }.to raise_error(StandardError)
|
|
|
|
end
|
|
|
|
|
2021-05-06 17:10:07 -04:00
|
|
|
it 'does not execute disabled hooks' do
|
2022-02-16 01:12:24 -05:00
|
|
|
allow(service_instance).to receive(:disabled?).and_return(true)
|
2021-05-06 17:10:07 -04:00
|
|
|
|
|
|
|
expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
|
|
|
|
end
|
|
|
|
|
2022-01-11 01:10:58 -05:00
|
|
|
it 'executes and registers the hook with the recursion detection', :aggregate_failures do
|
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
cache_key = Gitlab::WebHooks::RecursionDetection.send(:cache_key_for_hook, project_hook)
|
|
|
|
|
|
|
|
::Gitlab::Redis::SharedState.with do |redis|
|
|
|
|
expect { service_instance.execute }.to change {
|
|
|
|
redis.sismember(cache_key, project_hook.id)
|
|
|
|
}.to(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
|
|
|
|
.with(headers: headers)
|
|
|
|
.once
|
|
|
|
end
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
it 'blocks and logs if a recursive web hook is detected', :aggregate_failures do
|
2022-01-11 01:10:58 -05:00
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
Gitlab::WebHooks::RecursionDetection.register!(project_hook)
|
|
|
|
|
|
|
|
expect(Gitlab::AuthLogger).to receive(:error).with(
|
|
|
|
include(
|
2022-01-26 07:18:17 -05:00
|
|
|
message: 'Recursive webhook blocked from executing',
|
2022-01-11 01:10:58 -05:00
|
|
|
hook_id: project_hook.id,
|
|
|
|
hook_type: 'ProjectHook',
|
|
|
|
hook_name: 'push_hooks',
|
|
|
|
recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
|
|
|
|
'correlation_id' => kind_of(String)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
|
2022-01-11 01:10:58 -05:00
|
|
|
end
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
it 'blocks and logs if the recursion count limit would be exceeded', :aggregate_failures do
|
2022-01-11 01:10:58 -05:00
|
|
|
stub_full_request(project_hook.url, method: :post)
|
|
|
|
stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
|
|
|
|
previous_hooks = create_list(:project_hook, 3)
|
|
|
|
previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
|
|
|
|
|
|
|
|
expect(Gitlab::AuthLogger).to receive(:error).with(
|
|
|
|
include(
|
2022-01-26 07:18:17 -05:00
|
|
|
message: 'Recursive webhook blocked from executing',
|
2022-01-11 01:10:58 -05:00
|
|
|
hook_id: project_hook.id,
|
|
|
|
hook_type: 'ProjectHook',
|
|
|
|
hook_name: 'push_hooks',
|
|
|
|
recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
|
|
|
|
'correlation_id' => kind_of(String)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
|
2022-01-11 01:10:58 -05:00
|
|
|
end
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
it 'handles exceptions' do
|
2021-05-19 14:10:39 -04:00
|
|
|
exceptions = Gitlab::HTTP::HTTP_ERRORS + [
|
|
|
|
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError
|
2021-05-06 17:10:07 -04:00
|
|
|
]
|
2021-05-19 14:10:39 -04:00
|
|
|
|
2022-01-11 01:10:58 -05:00
|
|
|
allow(Gitlab::WebHooks::RecursionDetection).to receive(:block?).and_return(false)
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
exceptions.each do |exception_class|
|
|
|
|
exception = exception_class.new('Exception message')
|
2021-05-06 17:10:07 -04:00
|
|
|
project_hook.enable!
|
2017-04-27 06:08:57 -04:00
|
|
|
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_raise(exception)
|
2019-02-16 17:36:04 -05:00
|
|
|
expect(service_instance.execute).to eq({ status: :error, message: exception.to_s })
|
2017-04-27 06:08:57 -04:00
|
|
|
expect { service_instance.execute }.not_to raise_error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-01-12 13:11:03 -05:00
|
|
|
context 'when url is not encoded' do
|
2021-05-14 05:10:24 -04:00
|
|
|
let_it_be(:project_hook) { create(:project_hook, url: 'http://server.com/my path/') }
|
2021-01-12 13:11:03 -05:00
|
|
|
|
|
|
|
it 'handles exceptions' do
|
|
|
|
expect(service_instance.execute).to eq(status: :error, message: 'bad URI(is not URI?): "http://server.com/my path/"')
|
|
|
|
expect { service_instance.execute }.not_to raise_error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-08-12 11:10:02 -04:00
|
|
|
context 'when request body size is too big' do
|
|
|
|
it 'does not perform the request' do
|
|
|
|
stub_const("#{described_class}::REQUEST_BODY_SIZE_LIMIT", 10.bytes)
|
|
|
|
|
|
|
|
expect(service_instance.execute).to eq({ status: :error, message: "Gitlab::Json::LimitedEncoder::LimitExceeded" })
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
it 'handles 200 status code' do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
|
2017-04-27 06:08:57 -04:00
|
|
|
|
2017-07-20 11:12:06 -04:00
|
|
|
expect(service_instance.execute).to include({ status: :success, http_status: 200, message: 'Success' })
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'handles 2xx status codes' do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 201, body: 'Success')
|
2017-04-27 06:08:57 -04:00
|
|
|
|
2017-07-20 11:12:06 -04:00
|
|
|
expect(service_instance.execute).to include({ status: :success, http_status: 201, message: 'Success' })
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'execution logging' do
|
2022-06-07 14:09:27 -04:00
|
|
|
let(:default_log_data) do
|
|
|
|
{
|
|
|
|
trigger: 'push_hooks',
|
|
|
|
url: project_hook.url,
|
|
|
|
request_headers: headers,
|
|
|
|
request_data: data,
|
|
|
|
response_body: 'Success',
|
|
|
|
response_headers: {},
|
|
|
|
response_status: 200,
|
|
|
|
execution_duration: be > 0,
|
|
|
|
internal_error_message: nil
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2017-04-27 06:08:57 -04:00
|
|
|
context 'with success' do
|
|
|
|
before do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
|
2022-02-16 01:12:24 -05:00
|
|
|
context 'when forced' do
|
|
|
|
let(:service_instance) { described_class.new(project_hook, data, :push_hooks, force: true) }
|
|
|
|
|
|
|
|
it 'logs execution inline' do
|
|
|
|
expect(::WebHooks::LogExecutionWorker).not_to receive(:perform_async)
|
|
|
|
expect(::WebHooks::LogExecutionService)
|
|
|
|
.to receive(:new)
|
2022-06-07 14:09:27 -04:00
|
|
|
.with(hook: project_hook, log_data: default_log_data, response_category: :ok)
|
2022-02-16 01:12:24 -05:00
|
|
|
.and_return(double(execute: nil))
|
|
|
|
|
2022-02-22 22:17:13 -05:00
|
|
|
service_instance.execute
|
2022-02-16 01:12:24 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-02-22 22:17:13 -05:00
|
|
|
it 'queues LogExecutionWorker correctly' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
2022-06-07 14:09:27 -04:00
|
|
|
hash_including(default_log_data),
|
2022-02-22 22:17:13 -05:00
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
end
|
2021-05-06 17:10:07 -04:00
|
|
|
|
2022-02-22 22:17:13 -05:00
|
|
|
it 'queues LogExecutionWorker correctly, resulting in a log record (integration-style test)', :sidekiq_inline do
|
|
|
|
expect { service_instance.execute }.to change(::WebHookLog, :count).by(1)
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
2021-05-06 17:10:07 -04:00
|
|
|
|
2021-06-09 05:10:18 -04:00
|
|
|
it 'does not log in the service itself' do
|
|
|
|
expect { service_instance.execute }.not_to change(::WebHookLog, :count)
|
|
|
|
end
|
2021-05-06 17:10:07 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'with bad request' do
|
|
|
|
before do
|
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 400, body: 'Bad request')
|
|
|
|
end
|
|
|
|
|
2022-02-22 22:17:13 -05:00
|
|
|
it 'queues LogExecutionWorker correctly' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(
|
2022-06-07 14:09:27 -04:00
|
|
|
default_log_data.merge(
|
|
|
|
response_body: 'Bad request',
|
|
|
|
response_status: 400
|
|
|
|
)
|
2022-02-22 22:17:13 -05:00
|
|
|
),
|
|
|
|
:failed,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
2021-05-13 11:10:20 -04:00
|
|
|
end
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'with exception' do
|
|
|
|
before do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_raise(SocketError.new('Some HTTP Post error'))
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
|
2022-02-22 22:17:13 -05:00
|
|
|
it 'queues LogExecutionWorker correctly' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(
|
2022-06-07 14:09:27 -04:00
|
|
|
default_log_data.merge(
|
|
|
|
response_body: '',
|
|
|
|
response_status: 'internal error',
|
|
|
|
internal_error_message: 'Some HTTP Post error'
|
|
|
|
)
|
2022-02-22 22:17:13 -05:00
|
|
|
),
|
|
|
|
:error,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
2021-05-06 17:10:07 -04:00
|
|
|
end
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
|
2017-08-01 10:30:29 -04:00
|
|
|
context 'with unsafe response body' do
|
|
|
|
before do
|
2019-08-27 05:04:27 -04:00
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: "\xBB")
|
2017-08-01 10:30:29 -04:00
|
|
|
end
|
|
|
|
|
2022-02-22 22:17:13 -05:00
|
|
|
it 'queues LogExecutionWorker with sanitized response_body' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
2022-06-07 14:09:27 -04:00
|
|
|
hash_including(default_log_data.merge(response_body: '')),
|
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with oversize response body' do
|
|
|
|
let(:oversize_body) { 'a' * (described_class::RESPONSE_BODY_SIZE_LIMIT + 1) }
|
|
|
|
let(:stripped_body) { 'a' * (described_class::RESPONSE_BODY_SIZE_LIMIT - ellipsis.bytesize) + ellipsis }
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: oversize_body)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'queues LogExecutionWorker with stripped response_body' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(default_log_data.merge(response_body: stripped_body)),
|
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with massive amount of headers' do
|
|
|
|
let(:response_headers) do
|
|
|
|
(1..described_class::RESPONSE_HEADERS_COUNT_LIMIT + 1).to_a.to_h do |num|
|
|
|
|
["header-#{num}", SecureRandom.hex(num)]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:expected_response_headers) do
|
|
|
|
(1..described_class::RESPONSE_HEADERS_COUNT_LIMIT).to_a.to_h do |num|
|
|
|
|
# Capitalized
|
|
|
|
["Header-#{num}", response_headers["header-#{num}"]]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(
|
|
|
|
status: 200, body: 'Success', headers: response_headers
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'queues LogExecutionWorker with limited amount of headers' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(default_log_data.merge(response_headers: expected_response_headers)),
|
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with oversize header' do
|
|
|
|
let(:oversize_header) { 'a' * (described_class::RESPONSE_HEADERS_SIZE_LIMIT + 1) }
|
|
|
|
let(:stripped_header) { 'a' * (described_class::RESPONSE_HEADERS_SIZE_LIMIT - ellipsis.bytesize) + ellipsis }
|
|
|
|
let(:response_headers) { { 'oversized-header' => oversize_header } }
|
|
|
|
let(:expected_response_headers) { { 'Oversized-Header' => stripped_header } }
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(
|
|
|
|
status: 200, body: 'Success', headers: response_headers
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'queues LogExecutionWorker with stripped header value' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(default_log_data.merge(response_headers: expected_response_headers)),
|
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with log data exceeding Sidekiq limit' do
|
|
|
|
before do
|
|
|
|
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'queues LogExecutionWorker with request_data overrided in the second attempt' do
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(default_log_data),
|
2022-02-22 22:17:13 -05:00
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
2022-06-07 14:09:27 -04:00
|
|
|
.and_raise(
|
|
|
|
Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(WebHooks::LogExecutionWorker, 100, 50)
|
|
|
|
)
|
|
|
|
.ordered
|
|
|
|
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
|
|
|
|
.with(
|
|
|
|
project_hook.id,
|
|
|
|
hash_including(default_log_data.merge(request_data: WebHookLog::OVERSIZE_REQUEST_DATA)),
|
|
|
|
:ok,
|
|
|
|
nil
|
|
|
|
)
|
|
|
|
.and_call_original
|
|
|
|
.ordered
|
2022-02-22 22:17:13 -05:00
|
|
|
|
|
|
|
service_instance.execute
|
2017-08-01 10:30:29 -04:00
|
|
|
end
|
2022-06-07 14:09:27 -04:00
|
|
|
|
|
|
|
context 'new log data still exceeds limit' do
|
|
|
|
before do
|
|
|
|
allow(WebHooks::LogExecutionWorker).to receive(:perform_async).and_raise(
|
|
|
|
Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(WebHooks::LogExecutionWorker, 100, 50)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises an exception' do
|
|
|
|
expect do
|
|
|
|
service_instance.execute
|
|
|
|
end.to raise_error(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError)
|
|
|
|
end
|
|
|
|
end
|
2017-08-01 10:30:29 -04:00
|
|
|
end
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#async_execute' do
|
2021-05-14 05:10:24 -04:00
|
|
|
def expect_to_perform_worker(hook)
|
2022-01-28 10:14:45 -05:00
|
|
|
expect(WebHookWorker).to receive(:perform_async).with(hook.id, data, 'push_hooks', an_instance_of(Hash))
|
2021-05-14 05:10:24 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def expect_to_rate_limit(hook, threshold:, throttled: false)
|
|
|
|
expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?)
|
2022-06-16 05:09:15 -04:00
|
|
|
.with(:web_hook_calls, scope: [hook.parent.root_namespace], threshold: threshold)
|
2021-05-14 05:10:24 -04:00
|
|
|
.and_return(throttled)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when rate limiting is not configured' do
|
|
|
|
it 'queues a worker without tracking the call' do
|
|
|
|
expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
|
|
|
|
expect_to_perform_worker(project_hook)
|
|
|
|
|
|
|
|
service_instance.async_execute
|
|
|
|
end
|
|
|
|
end
|
2017-08-15 13:44:37 -04:00
|
|
|
|
2021-05-14 05:10:24 -04:00
|
|
|
context 'when rate limiting is configured' do
|
|
|
|
let_it_be(:threshold) { 3 }
|
|
|
|
let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: threshold) }
|
2017-04-27 06:08:57 -04:00
|
|
|
|
2021-05-14 05:10:24 -04:00
|
|
|
it 'queues a worker and tracks the call' do
|
|
|
|
expect_to_rate_limit(project_hook, threshold: threshold)
|
|
|
|
expect_to_perform_worker(project_hook)
|
|
|
|
|
|
|
|
service_instance.async_execute
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the hook is throttled (via mock)' do
|
|
|
|
before do
|
|
|
|
expect_to_rate_limit(project_hook, threshold: threshold, throttled: true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not queue a worker and logs an error' do
|
|
|
|
expect(WebHookWorker).not_to receive(:perform_async)
|
|
|
|
|
2021-06-14 11:09:48 -04:00
|
|
|
expect(Gitlab::AuthLogger).to receive(:error).with(
|
|
|
|
include(
|
|
|
|
message: 'Webhook rate limit exceeded',
|
|
|
|
hook_id: project_hook.id,
|
|
|
|
hook_type: 'ProjectHook',
|
|
|
|
hook_name: 'push_hooks',
|
|
|
|
"correlation_id" => kind_of(String),
|
|
|
|
"meta.project" => project.full_path,
|
|
|
|
"meta.related_class" => 'ProjectHook',
|
|
|
|
"meta.root_namespace" => project.root_namespace.full_path
|
|
|
|
)
|
|
|
|
)
|
2021-05-14 05:10:24 -04:00
|
|
|
|
|
|
|
service_instance.async_execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-06-16 05:09:15 -04:00
|
|
|
context 'when the hook is throttled (via Redis)', :clean_gitlab_redis_rate_limiting, :freeze_time do
|
2021-05-14 05:10:24 -04:00
|
|
|
before do
|
|
|
|
expect_to_perform_worker(project_hook).exactly(threshold).times
|
|
|
|
|
|
|
|
threshold.times { service_instance.async_execute }
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'stops queueing workers and logs errors' do
|
|
|
|
expect(Gitlab::AuthLogger).to receive(:error).twice
|
|
|
|
|
|
|
|
2.times { service_instance.async_execute }
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'still queues workers for other hooks' do
|
|
|
|
other_hook = create(:project_hook)
|
|
|
|
|
|
|
|
expect_to_perform_worker(other_hook)
|
|
|
|
|
|
|
|
described_class.new(other_hook, data, :push_hooks).async_execute
|
|
|
|
end
|
|
|
|
end
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
2021-05-20 08:10:26 -04:00
|
|
|
|
2022-01-11 01:10:58 -05:00
|
|
|
context 'recursion detection' do
|
|
|
|
before do
|
|
|
|
# Set a request UUID so `RecursionDetection.block?` will query redis.
|
|
|
|
Gitlab::WebHooks::RecursionDetection.set_request_uuid(SecureRandom.uuid)
|
|
|
|
end
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
it 'does not queue a worker and logs an error if the call chain limit would be exceeded' do
|
2022-01-11 01:10:58 -05:00
|
|
|
stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
|
|
|
|
previous_hooks = create_list(:project_hook, 3)
|
|
|
|
previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
expect(WebHookWorker).not_to receive(:perform_async)
|
2022-01-11 01:10:58 -05:00
|
|
|
expect(Gitlab::AuthLogger).to receive(:error).with(
|
|
|
|
include(
|
2022-01-26 07:18:17 -05:00
|
|
|
message: 'Recursive webhook blocked from executing',
|
2022-01-11 01:10:58 -05:00
|
|
|
hook_id: project_hook.id,
|
|
|
|
hook_type: 'ProjectHook',
|
|
|
|
hook_name: 'push_hooks',
|
|
|
|
recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
|
|
|
|
'correlation_id' => kind_of(String),
|
|
|
|
'meta.project' => project.full_path,
|
|
|
|
'meta.related_class' => 'ProjectHook',
|
|
|
|
'meta.root_namespace' => project.root_namespace.full_path
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.async_execute
|
|
|
|
end
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
it 'does not queue a worker and logs an error if a recursive call chain is detected' do
|
2022-01-11 01:10:58 -05:00
|
|
|
Gitlab::WebHooks::RecursionDetection.register!(project_hook)
|
|
|
|
|
2022-01-26 07:18:17 -05:00
|
|
|
expect(WebHookWorker).not_to receive(:perform_async)
|
2022-01-11 01:10:58 -05:00
|
|
|
expect(Gitlab::AuthLogger).to receive(:error).with(
|
|
|
|
include(
|
2022-01-26 07:18:17 -05:00
|
|
|
message: 'Recursive webhook blocked from executing',
|
2022-01-11 01:10:58 -05:00
|
|
|
hook_id: project_hook.id,
|
|
|
|
hook_type: 'ProjectHook',
|
|
|
|
hook_name: 'push_hooks',
|
|
|
|
recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
|
|
|
|
'correlation_id' => kind_of(String),
|
|
|
|
'meta.project' => project.full_path,
|
|
|
|
'meta.related_class' => 'ProjectHook',
|
|
|
|
'meta.root_namespace' => project.root_namespace.full_path
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
service_instance.async_execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-20 08:10:26 -04:00
|
|
|
context 'when hook has custom context attributes' do
|
|
|
|
it 'includes the attributes in the worker context' do
|
|
|
|
expect(WebHookWorker).to receive(:perform_async) do
|
|
|
|
expect(Gitlab::ApplicationContext.current).to include(
|
|
|
|
'meta.project' => project_hook.project.full_path,
|
|
|
|
'meta.root_namespace' => project.root_ancestor.path,
|
|
|
|
'meta.related_class' => 'ProjectHook'
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
service_instance.async_execute
|
|
|
|
end
|
|
|
|
end
|
2017-04-27 06:08:57 -04:00
|
|
|
end
|
|
|
|
end
|