diff options
Diffstat (limited to 'spec/support/shared_examples/requests')
4 files changed, 242 insertions, 8 deletions
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 1ad38a17f9c..acbcf4f7f3d 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -36,8 +36,8 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status expect(yaml_response.keys).to contain_exactly('apiVersion', 'entries', 'generated', 'serverInfo') expect(yaml_response['entries']).to be_a(Hash) - expect(yaml_response['entries'].keys).to contain_exactly(package.name) - expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project.id}/packages/helm" }) + expect(yaml_response['entries'].keys).to contain_exactly(package.name, package2.name) + expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project_id}/packages/helm" }) package_entry = yaml_response['entries'][package.name] @@ -45,6 +45,14 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls') expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db') expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"]) + + package_entry = yaml_response['entries'][package2.name] + + expect(package_entry.length).to eq(1) + expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls', 'description') + expect(package_entry.first['digest']).to eq('file2') + expect(package_entry.first['description']).to eq('hello from stable channel') + expect(package_entry.first['urls']).to eq(['charts/filename2.tgz']) end end end @@ -174,6 +182,13 @@ RSpec.shared_examples 'process helm download content request' do |user_type, sta context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + + expect_next_found_instance_of(::Packages::PackageFile) do |package_file| + expect(package_file).to receive(:file).and_wrap_original do |m, *args| + expect(package_file.id).to eq(package_file2.id) + m.call(*args) + end + end end it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package' @@ -189,7 +204,7 @@ end RSpec.shared_examples 'rejects helm access with unknown project id' do context 'with an unknown project' do - let(:project) { OpenStruct.new(id: 1234567890) } + let(:project_id) { 1234567890 } context 'as anonymous' do it_behaves_like 'rejects helm packages access', :anonymous, :unauthorized diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index 0390e60747f..2af7b616659 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -21,11 +21,24 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| expect(response).to match_response_schema('public_api/v4/packages/npm_package') expect(json_response['name']).to eq(package.name) expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') - ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any end expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') end + + it 'avoids N+1 database queries' do + control = ActiveRecord::QueryRecorder.new { get(url, headers: headers) } + + create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + create(:packages_dependency_link, package: package, dependency_type: dependency_type) + end + end + + # query count can slightly change between the examples so we're using a custom threshold + expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) + end end shared_examples 'reject metadata request' do |status:| diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index ecde4ee8565..eb650b7a09f 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -153,3 +153,15 @@ RSpec.shared_examples 'a package tracking event' do |category, action| expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) end end + +RSpec.shared_examples 'not a package tracking event' do + before do + stub_feature_flags(collect_package_events: true) + end + + it 'does not create a gitlab tracking event', :snowplow, :aggregate_failures do + expect { subject }.not_to change { Packages::Event.count } + + expect_no_snowplow_event + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 95817624658..2a19ff6f590 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # # Requires let variables: -# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api" +# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api", "throttle_authenticated_git_lfs", "throttle_authenticated_files_api" # * request_method # * request_args # * other_user_request_args @@ -14,7 +14,9 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do "throttle_protected_paths" => "throttle_authenticated_protected_paths_api", "throttle_authenticated_api" => "throttle_authenticated_api", "throttle_authenticated_web" => "throttle_authenticated_web", - "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api" + "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api", + "throttle_authenticated_git_lfs" => "throttle_authenticated_git_lfs", + "throttle_authenticated_files_api" => "throttle_authenticated_files_api" } end @@ -165,7 +167,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end # Requires let variables: -# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths" +# * throttle_setting_prefix: "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_git_lfs" # * user # * url_that_requires_authentication # * request_method @@ -176,7 +178,8 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do let(:throttle_types) do { "throttle_protected_paths" => "throttle_authenticated_protected_paths_web", - "throttle_authenticated_web" => "throttle_authenticated_web" + "throttle_authenticated_web" => "throttle_authenticated_web", + "throttle_authenticated_git_lfs" => "throttle_authenticated_git_lfs" } end @@ -385,3 +388,194 @@ RSpec.shared_examples 'tracking when dry-run mode is set' do end end end + +# Requires let variables: +# * throttle_name: "throttle_unauthenticated_api", "throttle_unauthenticated_web" +# * throttle_setting_prefix: "throttle_unauthenticated_api", "throttle_unauthenticated" +# * url_that_does_not_require_authentication +# * url_that_is_not_matched +# * requests_per_period +# * period_in_seconds +# * period +RSpec.shared_examples 'rate-limited unauthenticated requests' do + before do + # Set low limits + settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period + settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true + stub_application_setting(settings_to_set) + 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 } + 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 + expect(response).to have_gitlab_http_status(:ok) + end + + expect_rejection { get url_that_does_not_require_authentication } + + travel_to(period.from_now) do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + expect_rejection { get url_that_does_not_require_authentication } + end + end + + it 'counts requests from different IPs separately' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + expect_next_instance_of(Rack::Attack::Request) do |instance| + expect(instance).to receive(:ip).at_least(:once).and_return('1.2.3.4') + end + + # would be over limit for the same IP + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the request is not matched by the throttle' do + it 'does not throttle the requests' do + (1 + requests_per_period).times do + get url_that_is_not_matched + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the request is to the api internal endpoints' do + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get '/api/v4/internal/check', params: { secret_token: Gitlab::Shell.secret_token } + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the request is authenticated by a runner token' do + let(:request_jobs_url) { '/api/v4/jobs/request' } + let(:runner) { create(:ci_runner) } + + it 'does not count as unauthenticated' do + (1 + requests_per_period).times do + post request_jobs_url, params: { token: runner.token } + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'when the request is to a health endpoint' do + let(:health_endpoint) { '/-/metrics' } + + it 'does not throttle the requests' do + (1 + requests_per_period).times do + get health_endpoint + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the request is to a container registry notification endpoint' do + let(:secret_token) { 'secret_token' } + let(:events) { [{ action: 'push' }] } + let(:registry_endpoint) { '/api/v4/container_registry_event/events' } + let(:registry_headers) { { 'Content-Type' => ::API::ContainerRegistryEvent::DOCKER_DISTRIBUTION_EVENTS_V1_JSON } } + + before do + allow(Gitlab.config.registry).to receive(:notification_secret) { secret_token } + + event = spy(:event) + allow(::ContainerRegistry::Event).to receive(:new).and_return(event) + allow(event).to receive(:supported?).and_return(true) + end + + it 'does not throttle the requests' do + (1 + requests_per_period).times do + post registry_endpoint, + params: { events: events }.to_json, + headers: registry_headers.merge('Authorization' => secret_token) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + it 'logs RackAttack info into structured logs' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + arguments = a_hash_including({ + message: 'Rack_Attack', + env: :throttle, + remote_ip: '127.0.0.1', + request_method: 'GET', + path: url_that_does_not_require_authentication, + matched: throttle_name + }) + + expect(Gitlab::AuthLogger).to receive(:error).with(arguments) + + get url_that_does_not_require_authentication + end + + it_behaves_like 'tracking when dry-run mode is set' do + def do_request + get url_that_does_not_require_authentication + end + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + end + end +end |