Fix OAuth API and RSS rate limiting
This commit is contained in:
parent
d87030714a
commit
43a682ccaa
|
@ -125,7 +125,7 @@ class ApplicationController < ActionController::Base
|
||||||
# This filter handles private tokens, personal access tokens, and atom
|
# This filter handles private tokens, personal access tokens, and atom
|
||||||
# requests with rss tokens
|
# requests with rss tokens
|
||||||
def authenticate_sessionless_user!
|
def authenticate_sessionless_user!
|
||||||
user = Gitlab::Auth.find_sessionless_user(request)
|
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
|
||||||
|
|
||||||
sessionless_sign_in(user) if user
|
sessionless_sign_in(user) if user
|
||||||
>>>>>>> Add request throttles
|
>>>>>>> Add request throttles
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Rack::Attack
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticated_user_id
|
def authenticated_user_id
|
||||||
session_user_id || sessionless_user_id
|
Gitlab::Auth::RequestAuthenticator.new(self).user&.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_request?
|
def api_request?
|
||||||
|
@ -55,15 +55,5 @@ class Rack::Attack
|
||||||
def web_request?
|
def web_request?
|
||||||
!api_request?
|
!api_request?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def session_user_id
|
|
||||||
Gitlab::Auth.find_session_user(self)&.id
|
|
||||||
end
|
|
||||||
|
|
||||||
def sessionless_user_id
|
|
||||||
Gitlab::Auth.find_sessionless_user(self)&.id
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -82,36 +82,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# request may be Rack::Attack::Request which is just a Rack::Request, so
|
|
||||||
# we cannot use ActionDispatch::Request methods.
|
|
||||||
def find_user_by_private_token(request)
|
|
||||||
token = request.params['private_token'].presence || request.env['HTTP_PRIVATE_TOKEN'].presence
|
|
||||||
|
|
||||||
return unless token.present?
|
|
||||||
|
|
||||||
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
|
|
||||||
end
|
|
||||||
|
|
||||||
# request may be Rack::Attack::Request which is just a Rack::Request, so
|
|
||||||
# we cannot use ActionDispatch::Request methods.
|
|
||||||
def find_user_by_rss_token(request)
|
|
||||||
return unless request.params['format'] == 'atom'
|
|
||||||
|
|
||||||
token = request.params['rss_token'].presence
|
|
||||||
|
|
||||||
return unless token.present?
|
|
||||||
|
|
||||||
User.find_by_rss_token(token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_session_user(request)
|
|
||||||
request.env['warden']&.authenticate
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_sessionless_user(request)
|
|
||||||
find_user_by_private_token(request) || find_user_by_rss_token(request)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def service_request_check(login, password, project)
|
def service_request_check(login, password, project)
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Use for authentication only, in particular for Rack::Attack.
|
||||||
|
# Does not perform authorization of scopes, etc.
|
||||||
|
module Gitlab
|
||||||
|
module Auth
|
||||||
|
class RequestAuthenticator
|
||||||
|
def initialize(request)
|
||||||
|
@request = request
|
||||||
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
find_sessionless_user || find_session_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_sessionless_user
|
||||||
|
find_user_by_private_token || find_user_by_rss_token || find_user_by_oauth_token
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_session_user
|
||||||
|
@request.env['warden']&.authenticate if verified_request?
|
||||||
|
end
|
||||||
|
|
||||||
|
# request may be Rack::Attack::Request which is just a Rack::Request, so
|
||||||
|
# we cannot use ActionDispatch::Request methods.
|
||||||
|
def find_user_by_private_token
|
||||||
|
token = @request.params['private_token'].presence || @request.env['HTTP_PRIVATE_TOKEN'].presence
|
||||||
|
return unless token.present?
|
||||||
|
|
||||||
|
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
|
||||||
|
end
|
||||||
|
|
||||||
|
# request may be Rack::Attack::Request which is just a Rack::Request, so
|
||||||
|
# we cannot use ActionDispatch::Request methods.
|
||||||
|
def find_user_by_rss_token
|
||||||
|
return unless @request.path.ends_with?('atom') || @request.env['HTTP_ACCEPT'] == 'application/atom+xml'
|
||||||
|
|
||||||
|
token = @request.params['rss_token'].presence
|
||||||
|
return unless token.present?
|
||||||
|
|
||||||
|
User.find_by_rss_token(token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_user_by_oauth_token
|
||||||
|
access_token = find_oauth_access_token
|
||||||
|
access_token&.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_oauth_access_token
|
||||||
|
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
|
||||||
|
OauthAccessToken.by_token(token) if token
|
||||||
|
end
|
||||||
|
|
||||||
|
def doorkeeper_request
|
||||||
|
ActionDispatch::Request.new(@request.env)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if the request is GET/HEAD, or if CSRF token is valid.
|
||||||
|
def verified_request?
|
||||||
|
Gitlab::RequestForgeryProtection.verified?(@request.env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Rack::Attack do
|
describe Rack::Attack do
|
||||||
|
NUM_TRIES_FOR_REJECTION = 3 # Flaky tests, have not figured out how to fix it
|
||||||
|
|
||||||
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
|
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -22,6 +24,93 @@ describe Rack::Attack do
|
||||||
Timecop.freeze { example.run }
|
Timecop.freeze { example.run }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Requires let variables:
|
||||||
|
# * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
|
||||||
|
# * get_args
|
||||||
|
# * other_user_get_args
|
||||||
|
shared_examples_for 'rate-limited token-authenticated requests' do
|
||||||
|
let(:requests_per_period) { settings.send(:"#{throttle_setting_prefix}_requests_per_period") }
|
||||||
|
let(:period) { settings.send(:"#{throttle_setting_prefix}_period_in_seconds").seconds }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Set low limits
|
||||||
|
settings.send(:"#{throttle_setting_prefix}_requests_per_period=", 1)
|
||||||
|
settings.send(:"#{throttle_setting_prefix}_period_in_seconds=", 10000)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the throttle is enabled' do
|
||||||
|
before do
|
||||||
|
settings.send(:"#{throttle_setting_prefix}_enabled=", true)
|
||||||
|
settings.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects requests over the rate limit' do
|
||||||
|
# At first, allow requests under the rate limit.
|
||||||
|
requests_per_period.times do
|
||||||
|
get *get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
|
||||||
|
# the last straw
|
||||||
|
expect_rejection { get *get_args }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows requests after throttling and then waiting for the next period' do
|
||||||
|
requests_per_period.times do
|
||||||
|
get *get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
|
||||||
|
expect_rejection { get *get_args }
|
||||||
|
|
||||||
|
Timecop.travel((1.second + period).from_now) do # Add 1 because flaky
|
||||||
|
requests_per_period.times do
|
||||||
|
get *get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
|
||||||
|
expect_rejection { get *get_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'counts requests from different users separately, even from the same IP' do
|
||||||
|
requests_per_period.times do
|
||||||
|
get *get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
|
||||||
|
# would be over the limit if this wasn't a different user
|
||||||
|
get *other_user_get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'counts all requests from the same user, even via different IPs' do
|
||||||
|
requests_per_period.times do
|
||||||
|
get *get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
|
||||||
|
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
|
||||||
|
|
||||||
|
expect_rejection { get *get_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the throttle is disabled' do
|
||||||
|
before do
|
||||||
|
settings.send(:"#{throttle_setting_prefix}_enabled=", false)
|
||||||
|
settings.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows requests over the rate limit' do
|
||||||
|
(1 + requests_per_period).times do
|
||||||
|
get *get_args
|
||||||
|
expect(response).to have_http_status 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'unauthenticated requests' do
|
describe 'unauthenticated requests' do
|
||||||
let(:requests_per_period) { settings.throttle_unauthenticated_requests_per_period }
|
let(:requests_per_period) { settings.throttle_unauthenticated_requests_per_period }
|
||||||
let(:period) { settings.throttle_unauthenticated_period_in_seconds.seconds }
|
let(:period) { settings.throttle_unauthenticated_period_in_seconds.seconds }
|
||||||
|
@ -29,7 +118,7 @@ describe Rack::Attack do
|
||||||
before do
|
before do
|
||||||
# Set low limits
|
# Set low limits
|
||||||
settings.throttle_unauthenticated_requests_per_period = 1
|
settings.throttle_unauthenticated_requests_per_period = 1
|
||||||
settings.throttle_unauthenticated_period_in_seconds = 10
|
settings.throttle_unauthenticated_period_in_seconds = 10000
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the throttle is enabled' do
|
context 'when the throttle is enabled' do
|
||||||
|
@ -46,8 +135,7 @@ describe Rack::Attack do
|
||||||
end
|
end
|
||||||
|
|
||||||
# the last straw
|
# the last straw
|
||||||
get '/users/sign_in'
|
expect_rejection { get '/users/sign_in' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows requests after throttling and then waiting for the next period' do
|
it 'allows requests after throttling and then waiting for the next period' do
|
||||||
|
@ -56,17 +144,15 @@ describe Rack::Attack do
|
||||||
expect(response).to have_http_status 200
|
expect(response).to have_http_status 200
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/users/sign_in'
|
expect_rejection { get '/users/sign_in' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
|
|
||||||
Timecop.travel(period.from_now) do
|
Timecop.travel((1.second + period).from_now) do # Add 1 because flaky
|
||||||
requests_per_period.times do
|
requests_per_period.times do
|
||||||
get '/users/sign_in'
|
get '/users/sign_in'
|
||||||
expect(response).to have_http_status 200
|
expect(response).to have_http_status 200
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/users/sign_in'
|
expect_rejection { get '/users/sign_in' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,97 +185,101 @@ describe Rack::Attack do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'authenticated API requests', :api do
|
describe 'API requests authenticated with private token', :api do
|
||||||
let(:requests_per_period) { settings.throttle_authenticated_api_requests_per_period }
|
let(:requests_per_period) { settings.throttle_authenticated_api_requests_per_period }
|
||||||
let(:period) { settings.throttle_authenticated_api_period_in_seconds.seconds }
|
let(:period) { settings.throttle_authenticated_api_period_in_seconds.seconds }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
let(:other_user) { create(:user) }
|
||||||
|
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
|
||||||
|
|
||||||
before do
|
context 'with the token in the query string' do
|
||||||
# Set low limits
|
let(:get_args) { [api('/todos', user)] }
|
||||||
settings.throttle_authenticated_api_requests_per_period = 1
|
let(:other_user_get_args) { [api('/todos', other_user)] }
|
||||||
settings.throttle_authenticated_api_period_in_seconds = 10
|
|
||||||
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the throttle is enabled' do
|
context 'with the token in the headers' do
|
||||||
before do
|
let(:get_args) { ["/api/#{API::API.version}/todos", nil, private_token_headers(user)] }
|
||||||
settings.throttle_authenticated_api_enabled = true
|
let(:other_user_get_args) { ["/api/#{API::API.version}/todos", nil, private_token_headers(other_user)] }
|
||||||
settings.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects requests over the rate limit' do
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
# At first, allow requests under the rate limit.
|
|
||||||
requests_per_period.times do
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
end
|
|
||||||
|
|
||||||
# the last straw
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows requests after throttling and then waiting for the next period' do
|
|
||||||
requests_per_period.times do
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
end
|
|
||||||
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 429
|
|
||||||
|
|
||||||
Timecop.travel(period.from_now) do
|
|
||||||
requests_per_period.times do
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
end
|
|
||||||
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'counts requests from different users separately, even from the same IP' do
|
describe 'API requests authenticated with personal access token', :api do
|
||||||
other_user = create(:user)
|
let(:user) { create(:user) }
|
||||||
|
let(:token) { create(:personal_access_token, user: user) }
|
||||||
|
let(:other_user) { create(:user) }
|
||||||
|
let(:other_user_token) { create(:personal_access_token, user: other_user) }
|
||||||
|
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
|
||||||
|
|
||||||
requests_per_period.times do
|
context 'with the token in the query string' do
|
||||||
get api('/todos', user)
|
let(:get_args) { [api('/todos', personal_access_token: token)] }
|
||||||
expect(response).to have_http_status 200
|
let(:other_user_get_args) { [api('/todos', personal_access_token: other_user_token)] }
|
||||||
|
|
||||||
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
end
|
end
|
||||||
|
|
||||||
# would be over the limit if this wasn't a different user
|
context 'with the token in the headers' do
|
||||||
get api('/todos', other_user)
|
let(:get_args) { ["/api/#{API::API.version}/todos", nil, personal_access_token_headers(token)] }
|
||||||
expect(response).to have_http_status 200
|
let(:other_user_get_args) { ["/api/#{API::API.version}/todos", nil, personal_access_token_headers(other_user_token)] }
|
||||||
end
|
|
||||||
|
|
||||||
it 'counts all requests from the same user, even via different IPs' do
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
requests_per_period.times do
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
end
|
|
||||||
|
|
||||||
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
|
|
||||||
|
|
||||||
get api('/todos', user)
|
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the throttle is disabled' do
|
describe 'API requests authenticated with OAuth token', :api do
|
||||||
before do
|
let(:requests_per_period) { settings.throttle_authenticated_api_requests_per_period }
|
||||||
settings.throttle_authenticated_api_enabled = false
|
let(:period) { settings.throttle_authenticated_api_period_in_seconds.seconds }
|
||||||
settings.save!
|
let(:user) { create(:user) }
|
||||||
|
let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
|
||||||
|
let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
|
||||||
|
let(:other_user) { create(:user) }
|
||||||
|
let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) }
|
||||||
|
let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") }
|
||||||
|
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
|
||||||
|
|
||||||
|
context 'with the token in the query string' do
|
||||||
|
let(:get_args) { [api('/todos', oauth_access_token: token)] }
|
||||||
|
let(:other_user_get_args) { [api('/todos', oauth_access_token: other_user_token)] }
|
||||||
|
|
||||||
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows requests over the rate limit' do
|
context 'with the token in the headers' do
|
||||||
(1 + requests_per_period).times do
|
let(:get_args) { ["/api/#{API::API.version}/todos", nil, oauth_token_headers(token)] }
|
||||||
get api('/todos', user)
|
let(:other_user_get_args) { ["/api/#{API::API.version}/todos", nil, oauth_token_headers(other_user_token)] }
|
||||||
expect(response).to have_http_status 200
|
|
||||||
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '"web" (non-API) requests authenticated with RSS token' do
|
||||||
|
let(:requests_per_period) { settings.throttle_authenticated_web_requests_per_period }
|
||||||
|
let(:period) { settings.throttle_authenticated_web_period_in_seconds.seconds }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:other_user) { create(:user) }
|
||||||
|
let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
|
||||||
|
|
||||||
|
context 'with the token in the query string' do
|
||||||
|
context 'with the atom extension' do
|
||||||
|
let(:get_args) { ["/dashboard/projects.atom?rss_token=#{user.rss_token}"] }
|
||||||
|
let(:other_user_get_args) { ["/dashboard/projects.atom?rss_token=#{other_user.rss_token}"] }
|
||||||
|
|
||||||
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the atom format in the Accept header' do
|
||||||
|
let(:get_args) { ["/dashboard/projects?rss_token=#{user.rss_token}", nil, { 'HTTP_ACCEPT' => 'application/atom+xml' }] }
|
||||||
|
let(:other_user_get_args) { ["/dashboard/projects?rss_token=#{other_user.rss_token}", nil, { 'HTTP_ACCEPT' => 'application/atom+xml' }] }
|
||||||
|
|
||||||
|
it_behaves_like 'rate-limited token-authenticated requests'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'authenticated web requests' do
|
describe 'web requests authenticated with regular login' do
|
||||||
let(:requests_per_period) { settings.throttle_authenticated_web_requests_per_period }
|
let(:requests_per_period) { settings.throttle_authenticated_web_requests_per_period }
|
||||||
let(:period) { settings.throttle_authenticated_web_period_in_seconds.seconds }
|
let(:period) { settings.throttle_authenticated_web_period_in_seconds.seconds }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
@ -199,7 +289,7 @@ describe Rack::Attack do
|
||||||
|
|
||||||
# Set low limits
|
# Set low limits
|
||||||
settings.throttle_authenticated_web_requests_per_period = 1
|
settings.throttle_authenticated_web_requests_per_period = 1
|
||||||
settings.throttle_authenticated_web_period_in_seconds = 10
|
settings.throttle_authenticated_web_period_in_seconds = 10000
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the throttle is enabled' do
|
context 'when the throttle is enabled' do
|
||||||
|
@ -216,8 +306,7 @@ describe Rack::Attack do
|
||||||
end
|
end
|
||||||
|
|
||||||
# the last straw
|
# the last straw
|
||||||
get '/dashboard/snippets'
|
expect_rejection { get '/dashboard/snippets' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows requests after throttling and then waiting for the next period' do
|
it 'allows requests after throttling and then waiting for the next period' do
|
||||||
|
@ -226,17 +315,15 @@ describe Rack::Attack do
|
||||||
expect(response).to have_http_status 200
|
expect(response).to have_http_status 200
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/dashboard/snippets'
|
expect_rejection { get '/dashboard/snippets' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
|
|
||||||
Timecop.travel(period.from_now) do
|
Timecop.travel((1.second + period).from_now) do # Add 1 because flaky
|
||||||
requests_per_period.times do
|
requests_per_period.times do
|
||||||
get '/dashboard/snippets'
|
get '/dashboard/snippets'
|
||||||
expect(response).to have_http_status 200
|
expect(response).to have_http_status 200
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/dashboard/snippets'
|
expect_rejection { get '/dashboard/snippets' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -261,8 +348,7 @@ describe Rack::Attack do
|
||||||
|
|
||||||
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
|
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
|
||||||
|
|
||||||
get '/dashboard/snippets'
|
expect_rejection { get '/dashboard/snippets' }
|
||||||
expect(response).to have_http_status 429
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -280,4 +366,26 @@ describe Rack::Attack do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def private_token_headers(user)
|
||||||
|
{ 'HTTP_PRIVATE_TOKEN' => user.private_token }
|
||||||
|
end
|
||||||
|
|
||||||
|
def personal_access_token_headers(personal_access_token)
|
||||||
|
{ 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
|
||||||
|
end
|
||||||
|
|
||||||
|
def oauth_token_headers(oauth_access_token)
|
||||||
|
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_rejection(&block)
|
||||||
|
NUM_TRIES_FOR_REJECTION.times do |i|
|
||||||
|
block.call
|
||||||
|
break if response.status == 429 # success
|
||||||
|
Rails.logger.warn "Flaky test expected HTTP status 429 but got #{response.status}. Will attempt again (#{i + 1}/#{NUM_TRIES_FOR_REJECTION})"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(response).to have_http_status(429)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue